From 7fa13384d6904ef7f76e82e10c1398bdef38b056 Mon Sep 17 00:00:00 2001 From: Adeel Date: Sun, 5 Jan 2014 04:57:57 +0300 Subject: [PATCH] SASS: Added basic engine support. Initial commit for #418. --- EditorExtensions/CommandConstants.cs | 1 + .../CoffeeScript/CoffeeScriptCompiler.cs | 3 +- .../Compilers/SASS/SassCompiler.cs | 119 ++++++++++++++++++ .../Compilers/SASS/SassProjectCompiler.cs | 35 ++++++ EditorExtensions/EditorExtensionsPackage.cs | 2 +- EditorExtensions/Margin/CoffeeScriptMargin.cs | 5 - EditorExtensions/Margin/LessMargin.cs | 8 -- EditorExtensions/Margin/MarginBase.cs | 19 ++- EditorExtensions/Margin/MarkdownMargin.cs | 5 - EditorExtensions/Margin/SassMargin.cs | 76 +++++++++++ EditorExtensions/Margin/SvgMargin.cs | 10 +- EditorExtensions/Margin/TypeScriptMargin.cs | 5 - EditorExtensions/MenuItems/BuildMenu.cs | 30 ++++- .../Options/WebEssentialsSettings.cs | 9 ++ EditorExtensions/PreBuildTask.cs | 1 + EditorExtensions/WebEssentials2013.csproj | 11 +- 16 files changed, 292 insertions(+), 47 deletions(-) create mode 100644 EditorExtensions/Compilers/SASS/SassCompiler.cs create mode 100644 EditorExtensions/Compilers/SASS/SassProjectCompiler.cs create mode 100644 EditorExtensions/Margin/SassMargin.cs diff --git a/EditorExtensions/CommandConstants.cs b/EditorExtensions/CommandConstants.cs index 2ba76835e..05fbaa553 100644 --- a/EditorExtensions/CommandConstants.cs +++ b/EditorExtensions/CommandConstants.cs @@ -91,6 +91,7 @@ enum CommandId // Build BuildBundles = 0x1083, BuildLess = 0x1084, + BuildSass = 0x1085, BuildMinify = 0x1086, BuildCoffeeScript = 0x1087, diff --git a/EditorExtensions/Compilers/CoffeeScript/CoffeeScriptCompiler.cs b/EditorExtensions/Compilers/CoffeeScript/CoffeeScriptCompiler.cs index 3823f7cf2..9fd428ba1 100644 --- a/EditorExtensions/Compilers/CoffeeScript/CoffeeScriptCompiler.cs +++ b/EditorExtensions/Compilers/CoffeeScript/CoffeeScriptCompiler.cs @@ -29,10 +29,11 @@ protected override Regex ErrorParsingPattern protected override string GetArguments(string sourceFileName, string targetFileName) { var args = new StringBuilder(); + if (WESettings.GetBoolean(WESettings.Keys.WrapCoffeeScriptClosure)) args.Append("--bare "); - if (WESettings.GetBoolean(WESettings.Keys.CoffeeScriptSourceMaps)) + if (WESettings.GetBoolean(WESettings.Keys.CoffeeScriptSourceMaps) && !InUnitTests) args.Append("--map "); args.AppendFormat(CultureInfo.CurrentCulture, "--output \"{0}\" --compile \"{1}\"", Path.GetDirectoryName(targetFileName), sourceFileName); diff --git a/EditorExtensions/Compilers/SASS/SassCompiler.cs b/EditorExtensions/Compilers/SASS/SassCompiler.cs new file mode 100644 index 000000000..a6d1c415d --- /dev/null +++ b/EditorExtensions/Compilers/SASS/SassCompiler.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Web.Helpers; +using MadsKristensen.EditorExtensions.Helpers; +using Microsoft.CSS.Core; + +namespace MadsKristensen.EditorExtensions +{ + public class SassCompiler : NodeExecutorBase + { + private static readonly string _compilerPath = Path.Combine(WebEssentialsResourceDirectory, @"nodejs\node_modules\node-sass\bin\node-sass"); + private static readonly Regex _endingCurlyBraces = new Regex(@"}\W*}|}", RegexOptions.Compiled); + private static readonly Regex _linesStartingWithTwoSpaces = new Regex("(\n( *))", RegexOptions.Compiled); + private static readonly Regex _errorParsingPattern = new Regex(@"^(?.+) in (?.+) on line (?\d+), column (?\d+):$", RegexOptions.Multiline); + private static readonly Regex _sourceMapInCss = new Regex(@"\/\*#([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/", RegexOptions.Multiline); + + protected override string ServiceName + { + get { return "SASS"; } + } + protected override string CompilerPath + { + get { return _compilerPath; } + } + protected override Regex ErrorParsingPattern + { + get { return _errorParsingPattern; } + } + protected override string GetArguments(string sourceFileName, string targetFileName) + { + var args = new StringBuilder("--no-color --relative-urls "); + + if (WESettings.GetBoolean(WESettings.Keys.SassSourceMaps) && !InUnitTests) + { + args.Append("--source-comments=map"); + } + + args.AppendFormat(CultureInfo.CurrentCulture, "\"{0}\" \"{1}\"", sourceFileName, targetFileName); + return args.ToString(); + } + + protected override string PostProcessResult(string resultSource, string sourceFileName, string targetFileName) + { + // Inserts an empty row between each rule and replace two space indentation with 4 space indentation + resultSource = _endingCurlyBraces.Replace(_linesStartingWithTwoSpaces.Replace(resultSource.Trim(), "$1$2"), "$&\n"); + resultSource = UpdateSourceMapUrls(resultSource, targetFileName); + + var message = "SASS: " + Path.GetFileName(sourceFileName) + " compiled."; + + // If the caller wants us to renormalize URLs to a different filename, do so. + if (targetFileName != null && resultSource.IndexOf("url(", StringComparison.OrdinalIgnoreCase) > 0) + { + try + { + resultSource = CssUrlNormalizer.NormalizeUrls( + tree: new CssParser().Parse(resultSource, true), + targetFile: targetFileName, + oldBasePath: sourceFileName + ); + } + catch (Exception ex) + { + message = "SASS: An error occurred while normalizing generated paths in " + sourceFileName + "\r\n" + ex; + } + } + + Logger.Log(message); + + return resultSource; + } + + private static string UpdateSourceMapUrls(string content, string compiledFileName) + { + if (!WESettings.GetBoolean(WESettings.Keys.SassSourceMaps) || !File.Exists(compiledFileName)) + return content; + + string sourceMapFilename = compiledFileName + ".map"; + + if (!File.Exists(sourceMapFilename)) + return content; + + var updatedFileContent = GetUpdatedSourceMapFileContent(compiledFileName, sourceMapFilename); + + if (updatedFileContent == null) + return content; + + FileHelpers.WriteFile(updatedFileContent, sourceMapFilename); + ProjectHelpers.AddFileToProject(compiledFileName, sourceMapFilename); + + return UpdateSourceLinkInCssComment(content, FileHelpers.RelativePath(compiledFileName, sourceMapFilename)); + } + + private static string GetUpdatedSourceMapFileContent(string cssFileName, string sourceMapFilename) + { + // Read JSON map file and deserialize. + dynamic jsonSourceMap = Json.Decode(File.ReadAllText(sourceMapFilename)); + + if (jsonSourceMap == null) + return null; + + jsonSourceMap.sources = ((IEnumerable)jsonSourceMap.sources).Select(s => FileHelpers.RelativePath(cssFileName, s)); + jsonSourceMap.names = new List(jsonSourceMap.names); + jsonSourceMap.file = Path.GetFileName(cssFileName); + + return Json.Encode(jsonSourceMap); + } + + private static string UpdateSourceLinkInCssComment(string content, string sourceMapRelativePath) + { // Fix sourceMappingURL comment in CSS file with network accessible path. + return _sourceMapInCss.Replace(content, + string.Format(CultureInfo.CurrentCulture, "/*# sourceMappingURL={0} */", sourceMapRelativePath)); + } + } +} \ No newline at end of file diff --git a/EditorExtensions/Compilers/SASS/SassProjectCompiler.cs b/EditorExtensions/Compilers/SASS/SassProjectCompiler.cs new file mode 100644 index 000000000..cf3ffcfc8 --- /dev/null +++ b/EditorExtensions/Compilers/SASS/SassProjectCompiler.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace MadsKristensen.EditorExtensions +{ + internal class SassProjectCompiler : ProjectCompilerBase + { + protected override string ServiceName + { + get { return "SASS"; } + } + + protected override string CompileToExtension + { + get { return ".css"; } + } + + protected override string CompileToLocation + { + get { return WESettings.GetString(WESettings.Keys.SassCompileToLocation); } + } + + protected override NodeExecutorBase Compiler + { + get { return new SassCompiler(); } + } + + protected override IEnumerable Extensions + { + get + { + return new string[] { ".scss" }; + } + } + } +} \ No newline at end of file diff --git a/EditorExtensions/EditorExtensionsPackage.cs b/EditorExtensions/EditorExtensionsPackage.cs index 576c515a7..a7808f513 100644 --- a/EditorExtensions/EditorExtensionsPackage.cs +++ b/EditorExtensions/EditorExtensionsPackage.cs @@ -156,7 +156,7 @@ private void HandleMenuVisibility(OleMenuCommandService mcs) OleMenuCommand menuCommand = new OleMenuCommand((s, e) => { }, commandId); menuCommand.BeforeQueryStatus += menuCommand_BeforeQueryStatus; mcs.AddCommand(menuCommand); - + CommandID cmdTopMenu = new CommandID(CommandGuids.guidTopMenu, (int)CommandId.TopMenu); _topMenu = new OleMenuCommand((s, e) => { }, cmdTopMenu); mcs.AddCommand(_topMenu); diff --git a/EditorExtensions/Margin/CoffeeScriptMargin.cs b/EditorExtensions/Margin/CoffeeScriptMargin.cs index be9427220..6d4d7c9bd 100644 --- a/EditorExtensions/Margin/CoffeeScriptMargin.cs +++ b/EditorExtensions/Margin/CoffeeScriptMargin.cs @@ -79,10 +79,5 @@ public override bool IsSaveFileEnabled { get { return WESettings.GetBoolean(WESettings.Keys.GenerateJsFileFromCoffeeScript); } } - - protected override bool CanWriteToDisk(string source) - { - return !string.IsNullOrWhiteSpace(source); - } } } \ No newline at end of file diff --git a/EditorExtensions/Margin/LessMargin.cs b/EditorExtensions/Margin/LessMargin.cs index 8897b59e4..7b9b0ea01 100644 --- a/EditorExtensions/Margin/LessMargin.cs +++ b/EditorExtensions/Margin/LessMargin.cs @@ -72,13 +72,5 @@ public override bool IsSaveFileEnabled { get { return WESettings.GetBoolean(WESettings.Keys.GenerateCssFileFromLess) && !Path.GetFileName(Document.FilePath).StartsWith("_", StringComparison.Ordinal); } } - - protected override bool CanWriteToDisk(string source) - { - //var parser = new Microsoft.CSS.Core.CssParser(); - //StyleSheet stylesheet = parser.Parse(source, false); - - return true;// !string.IsNullOrWhiteSpace(stylesheet.Text); - } } } \ No newline at end of file diff --git a/EditorExtensions/Margin/MarginBase.cs b/EditorExtensions/Margin/MarginBase.cs index a03645559..89bfb7fad 100644 --- a/EditorExtensions/Margin/MarginBase.cs +++ b/EditorExtensions/Margin/MarginBase.cs @@ -19,12 +19,18 @@ public abstract class MarginBase : DockPanel, IWpfTextViewMargin private bool _isDisposed = false; private IWpfTextViewHost _viewHost; private string _marginName; - protected string SettingsKey { get; private set; } private bool _showMargin; - protected bool IsFirstRun { get; private set; } private Dispatcher _dispatcher; private ErrorListProvider _provider; + protected string SettingsKey { get; private set; } + protected bool IsFirstRun { get; private set; } + public abstract bool IsSaveFileEnabled { get; } + public abstract bool CompileEnabled { get; } + public abstract string CompileToLocation { get; } + protected ITextDocument Document { get; set; } + protected virtual bool CanWriteToDisk { get { return true; } } + protected MarginBase() { _dispatcher = Dispatcher.CurrentDispatcher; @@ -61,11 +67,6 @@ void Document_FileActionOccurred(object sender, TextDocumentFileActionEventArgs } } - public abstract bool IsSaveFileEnabled { get; } - public abstract bool CompileEnabled { get; } - public abstract string CompileToLocation { get; } - protected ITextDocument Document { get; set; } - private void Initialize(string contentType, string source) { _viewHost = CreateTextViewHost(contentType); @@ -257,7 +258,7 @@ private static string GetTargetFileName(string sourceFileName, string CompileToL private void WriteCompiledFile(string content, string currentFileName, string fileName) { if (ProjectHelpers.CheckOutFileFromSourceControl(fileName) - && CanWriteToDisk(content) + && CanWriteToDisk && FileHelpers.WriteFile(content, fileName)) { ProjectHelpers.AddFileToProject(currentFileName, fileName); @@ -336,8 +337,6 @@ private void task_Navigate(object sender, EventArgs e) } } - protected abstract bool CanWriteToDisk(string source); - private void ThrowIfDisposed() { if (_isDisposed) diff --git a/EditorExtensions/Margin/MarkdownMargin.cs b/EditorExtensions/Margin/MarkdownMargin.cs index 8bf113642..cfd464caf 100644 --- a/EditorExtensions/Margin/MarkdownMargin.cs +++ b/EditorExtensions/Margin/MarkdownMargin.cs @@ -161,11 +161,6 @@ public override bool IsSaveFileEnabled get { return true; } } - protected override bool CanWriteToDisk(string source) - { - return true; - } - public override bool CompileEnabled { get { return WESettings.GetBoolean(WESettings.Keys.MarkdownEnableCompiler); } diff --git a/EditorExtensions/Margin/SassMargin.cs b/EditorExtensions/Margin/SassMargin.cs new file mode 100644 index 000000000..5cba80e1a --- /dev/null +++ b/EditorExtensions/Margin/SassMargin.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using System.Linq; +using EnvDTE; +using Microsoft.VisualStudio.Text; + +namespace MadsKristensen.EditorExtensions +{ + public class SassMargin : MarginBase + { + public const string MarginName = "SassMargin"; + + public SassMargin(string contentType, string source, bool showMargin, ITextDocument document) + : base(source, MarginName, contentType, showMargin, document) + { } + + protected override async void StartCompiler(string source) + { + if (!CompileEnabled) + return; + + string sassFilePath = Document.FilePath; + + string cssFilename = GetCompiledFileName(sassFilePath, ".css", CompileEnabled ? CompileToLocation : null); + + if (IsFirstRun && File.Exists(cssFilename)) + { + OnCompilationDone(File.ReadAllText(cssFilename), sassFilePath); + return; + } + + Logger.Log("SASS: Compiling " + Path.GetFileName(sassFilePath)); + + var result = await new SassCompiler().Compile(sassFilePath, cssFilename); + + if (result.IsSuccess) + { + OnCompilationDone(result.Result, result.FileName); + } + else + { + result.Errors.First().Message = "SASS: " + result.Errors.First().Message; + + CreateTask(result.Errors.First()); + + base.OnCompilationDone("ERROR:" + result.Errors.First().Message, sassFilePath); + } + } + + protected override void MinifyFile(string fileName, string source) + { + if (!CompileEnabled) + return; + + if (WESettings.GetBoolean(WESettings.Keys.SassMinify) && !Path.GetFileName(fileName).StartsWith("_", StringComparison.Ordinal)) + { + FileHelpers.MinifyFile(fileName, source, ".css"); + } + } + + public override bool CompileEnabled + { + get { return WESettings.GetBoolean(WESettings.Keys.SassEnableCompiler); } + } + + public override string CompileToLocation + { + get { return WESettings.GetString(WESettings.Keys.SassCompileToLocation); } + } + + public override bool IsSaveFileEnabled + { + get { return WESettings.GetBoolean(WESettings.Keys.GenerateCssFileFromSass) && !Path.GetFileName(Document.FilePath).StartsWith("_", StringComparison.Ordinal); } + } + } +} \ No newline at end of file diff --git a/EditorExtensions/Margin/SvgMargin.cs b/EditorExtensions/Margin/SvgMargin.cs index c85344a2b..853bfa0b3 100644 --- a/EditorExtensions/Margin/SvgMargin.cs +++ b/EditorExtensions/Margin/SvgMargin.cs @@ -12,6 +12,11 @@ internal class SvgMargin : MarginBase private WebBrowser _browser; private string _fileName; + protected override bool CanWriteToDisk + { + get { return false; } + } + public SvgMargin(string contentType, string source, bool showMargin, ITextDocument document) : base(source, MarginName, contentType, showMargin, document) { @@ -74,11 +79,6 @@ public override bool IsSaveFileEnabled get { return false; } } - protected override bool CanWriteToDisk(string source) - { - return false; - } - public override bool CompileEnabled { get { return true; } diff --git a/EditorExtensions/Margin/TypeScriptMargin.cs b/EditorExtensions/Margin/TypeScriptMargin.cs index 693cd7c6c..237fe7e18 100644 --- a/EditorExtensions/Margin/TypeScriptMargin.cs +++ b/EditorExtensions/Margin/TypeScriptMargin.cs @@ -90,11 +90,6 @@ public override bool IsSaveFileEnabled get { return false; } } - protected override bool CanWriteToDisk(string source) - { - return false; - } - public override bool CompileEnabled { get { return false; } diff --git a/EditorExtensions/MenuItems/BuildMenu.cs b/EditorExtensions/MenuItems/BuildMenu.cs index 1f9860379..7a9dacf90 100644 --- a/EditorExtensions/MenuItems/BuildMenu.cs +++ b/EditorExtensions/MenuItems/BuildMenu.cs @@ -31,6 +31,10 @@ public void SetupCommands() OleMenuCommand menuLess = new OleMenuCommand(async (s, e) => await BuildLess(), cmdLess); _mcs.AddCommand(menuLess); + CommandID cmdSass = new CommandID(CommandGuids.guidBuildCmdSet, (int)CommandId.BuildSass); + OleMenuCommand menuSass = new OleMenuCommand(async (s, e) => await BuildSass(), cmdSass); + _mcs.AddCommand(menuSass); + CommandID cmdMinify = new CommandID(CommandGuids.guidBuildCmdSet, (int)CommandId.BuildMinify); OleMenuCommand menuMinify = new OleMenuCommand((s, e) => Minify(), cmdMinify); _mcs.AddCommand(menuMinify); @@ -64,20 +68,27 @@ public async static ThreadingTasks.Task BuildCoffeeScript() EditorExtensionsPackage.DTE.StatusBar.Clear(); } - public static void UpdateBundleFiles() + public async static ThreadingTasks.Task BuildLess() { - EditorExtensionsPackage.DTE.StatusBar.Text = "Updating bundles..."; - BundleFilesMenu.UpdateBundles(null, true); + EditorExtensionsPackage.DTE.StatusBar.Text = "Compiling LESS..."; + + var projectTasks = ProjectHelpers.GetAllProjects().Select(project => + { + return new LessProjectCompiler().CompileProject(project); + }); + + await ThreadingTasks.Task.WhenAll(projectTasks.ToArray()); + EditorExtensionsPackage.DTE.StatusBar.Clear(); } - public async static ThreadingTasks.Task BuildLess() + public async static ThreadingTasks.Task BuildSass() { - EditorExtensionsPackage.DTE.StatusBar.Text = "Compiling LESS..."; + EditorExtensionsPackage.DTE.StatusBar.Text = "Compiling SASS..."; var projectTasks = ProjectHelpers.GetAllProjects().Select(project => { - return new LessProjectCompiler().CompileProject(project); + return new SassProjectCompiler().CompileProject(project); }); await ThreadingTasks.Task.WhenAll(projectTasks.ToArray()); @@ -85,6 +96,13 @@ public async static ThreadingTasks.Task BuildLess() EditorExtensionsPackage.DTE.StatusBar.Clear(); } + public static void UpdateBundleFiles() + { + EditorExtensionsPackage.DTE.StatusBar.Text = "Updating bundles..."; + BundleFilesMenu.UpdateBundles(null, true); + EditorExtensionsPackage.DTE.StatusBar.Clear(); + } + private static void Minify() { _dte.StatusBar.Text = "Web Essentials: Minifying files..."; diff --git a/EditorExtensions/Options/WebEssentialsSettings.cs b/EditorExtensions/Options/WebEssentialsSettings.cs index 2dd923470..5539b3406 100644 --- a/EditorExtensions/Options/WebEssentialsSettings.cs +++ b/EditorExtensions/Options/WebEssentialsSettings.cs @@ -25,6 +25,15 @@ public static class Keys public const string LessEnableCompiler = "LessEnableCompiler"; public const string LessCompileToLocation = "LessCompileToLocation"; + // SASS + public const string GenerateCssFileFromSass = "SassGenerateCssFile"; + public const string ShowSassPreviewWindow = "SassShowPreviewWindow"; + public const string SassMinify = "SassMinify"; + public const string SassCompileOnBuild = "SassCompileOnBuild"; + public const string SassSourceMaps = "SassSourceMaps"; + public const string SassEnableCompiler = "SassEnableCompiler"; + public const string SassCompileToLocation = "SassCompileToLocation"; + // TypeScript public const string ShowTypeScriptPreviewWindow = "TypeScriptShowPreviewWindow"; diff --git a/EditorExtensions/PreBuildTask.cs b/EditorExtensions/PreBuildTask.cs index 2f21fcba2..cde6631c6 100644 --- a/EditorExtensions/PreBuildTask.cs +++ b/EditorExtensions/PreBuildTask.cs @@ -36,6 +36,7 @@ public override bool Execute() return Task.WhenAll( InstallModuleAsync("lessc", "less"), InstallModuleAsync("jshint", "jshint"), + InstallModuleAsync("node-sass", "node-sass"), InstallModuleAsync("coffee", "coffee-script"), InstallModuleAsync("iced", "iced-coffee-script") ).Result.All(b => b); diff --git a/EditorExtensions/WebEssentials2013.csproj b/EditorExtensions/WebEssentials2013.csproj index 326b5a7a9..fd381a1f6 100644 --- a/EditorExtensions/WebEssentials2013.csproj +++ b/EditorExtensions/WebEssentials2013.csproj @@ -367,6 +367,9 @@ + + + @@ -386,6 +389,7 @@ + @@ -611,7 +615,6 @@ - @@ -990,6 +993,12 @@ true + + true + + + true + true