diff --git a/SassAndCoffee.AspNet/CompilableFileHandler.cs b/SassAndCoffee.AspNet/CompilableFileHandler.cs index 677eb04..164439d 100644 --- a/SassAndCoffee.AspNet/CompilableFileHandler.cs +++ b/SassAndCoffee.AspNet/CompilableFileHandler.cs @@ -3,7 +3,6 @@ namespace SassAndCoffee.AspNet { using System; - using System.IO; using System.Web; using SassAndCoffee.Core; @@ -25,16 +24,8 @@ public bool IsReusable { public void ProcessRequest(HttpContext context) { - var fi = new FileInfo(context.Request.PhysicalPath); - var requestedFileName = fi.FullName; - - if (fi.Exists) { - BuildHeaders(context.Response, _contentCompiler.GetOutputMimeType(requestedFileName), fi.LastWriteTimeUtc); - context.Response.WriteFile(requestedFileName); - return; - } - - var compilationResult = _contentCompiler.GetCompiledContent(context.Request.Path); + VirtualPathCompilerFile file = new VirtualPathCompilerFile(context.Request.Path); + var compilationResult = _contentCompiler.GetCompiledContent(file); if (compilationResult.Compiled == false) { context.Response.StatusCode = 404; return; @@ -49,7 +40,7 @@ static void BuildHeaders(HttpResponse response, string mimeType, DateTime lastMo response.StatusCode = 200; response.Filter = new GZipStream(response.Filter, CompressionMode.Compress); response.AddHeader("content-encoding", "gzip"); - response.Cache.VaryByHeaders["Accept-encoding"] = true; + response.Cache.VaryByHeaders["Accept-Encoding"] = true; response.AddHeader("ETag", lastModified.Ticks.ToString("x")); response.AddHeader("Content-Type", mimeType); response.AddHeader("Content-Disposition", "inline"); diff --git a/SassAndCoffee.AspNet/CompilableFileModule.cs b/SassAndCoffee.AspNet/CompilableFileModule.cs index 0f7cba9..8164b6b 100644 --- a/SassAndCoffee.AspNet/CompilableFileModule.cs +++ b/SassAndCoffee.AspNet/CompilableFileModule.cs @@ -9,7 +9,7 @@ namespace SassAndCoffee.AspNet using SassAndCoffee.Core.Caching; using System.Configuration; - public class CompilableFileModule : IHttpModule, ICompilerHost + public class CompilableFileModule : IHttpModule { IContentCompiler _compiler; IHttpHandler _handler; @@ -29,7 +29,7 @@ public void Init(HttpApplication context) _compiler = _compiler ?? initializeCompilerFromSettings(cacheType); _handler = _handler ?? new CompilableFileHandler(_compiler); - if (!_compiler.CanCompile(app.Request.Path)) { + if (!_compiler.CanCompile(new VirtualPathCompilerFile(app.Request.Path))) { return; } @@ -42,23 +42,19 @@ public string MapPath(string path) return HttpContext.Current.Server.MapPath(path); } - IContentCompiler initializeCompilerFromSettings(string cacheType) - { + IContentCompiler initializeCompilerFromSettings(string cacheType) { if (string.Equals(cacheType, "NoCache", System.StringComparison.InvariantCultureIgnoreCase)) { // NoCache - return new ContentCompiler(this, new NoCache()); - } else if (string.Equals(cacheType, "InMemoryCache", System.StringComparison.InvariantCultureIgnoreCase)) { + return new ContentCompiler(new NoCache()); + } + if (string.Equals(cacheType, "InMemoryCache", System.StringComparison.InvariantCultureIgnoreCase)) { // InMemoryCache - return new ContentCompiler(this, new InMemoryCache()); - } else { - // FileCache - var cachePath = Path.Combine(HostingEnvironment.MapPath("~/App_Data"), "_FileCache"); - if (!Directory.Exists(cachePath)) { - Directory.CreateDirectory(cachePath); - } - - return new ContentCompiler(this, new FileCache(cachePath)); + return new ContentCompiler(new InMemoryCache()); } + // FileCache + var cachePath = Path.Combine(HostingEnvironment.MapPath("~/App_Data"), "_FileCache"); + Directory.CreateDirectory(cachePath); + return new ContentCompiler(new FileCache(cachePath)); } public void Dispose() diff --git a/SassAndCoffee.AspNet/SassAndCoffee.AspNet.csproj b/SassAndCoffee.AspNet/SassAndCoffee.AspNet.csproj index a41210c..8c5bf3d 100644 --- a/SassAndCoffee.AspNet/SassAndCoffee.AspNet.csproj +++ b/SassAndCoffee.AspNet/SassAndCoffee.AspNet.csproj @@ -45,6 +45,7 @@ + diff --git a/SassAndCoffee.AspNet/VirtualPathCompilerFile.cs b/SassAndCoffee.AspNet/VirtualPathCompilerFile.cs new file mode 100644 index 0000000..726b3e2 --- /dev/null +++ b/SassAndCoffee.AspNet/VirtualPathCompilerFile.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using System.Web.Hosting; + +using SassAndCoffee.Core; + +namespace SassAndCoffee.AspNet { + internal class VirtualPathCompilerFile: ICompilerFile { + private static readonly DateTime unknownFileTime = DateTime.UtcNow; + + private readonly string virtualPath; + + public VirtualPathCompilerFile(string virtualPath) { + if (string.IsNullOrEmpty(virtualPath)) { + throw new ArgumentNullException("virtualPath"); + } + this.virtualPath = virtualPath; + } + + public DateTime LastWriteTimeUtc { + get { + if (!Exists) { + return default(DateTime); + } + using (Stream stream = Open()) { + FileStream file = stream as FileStream; + if (file != null) { + return File.GetLastWriteTimeUtc(file.Name); + } + } + // if the stream is not a file stream, we cannot determine the last write time and take the app startup time instead to avoid caching issues + return unknownFileTime; + } + } + + public Stream Open() + { + return HostingEnvironment.VirtualPathProvider.GetFile(virtualPath).Open(); + } + + public string Name { + get { + return virtualPath; + } + } + + public bool Exists { + get { + return HostingEnvironment.VirtualPathProvider.FileExists(virtualPath); + } + } + + public ICompilerFile GetRelativeFile(string relativePath) + { + return new VirtualPathCompilerFile(HostingEnvironment.VirtualPathProvider.CombineVirtualPaths(virtualPath, relativePath.Replace('\\', '/'))); + } + } +} diff --git a/SassAndCoffee.Core.Tests/SassAndCoffee.Core.Tests.csproj b/SassAndCoffee.Core.Tests/SassAndCoffee.Core.Tests.csproj index 0c575a0..556b956 100644 --- a/SassAndCoffee.Core.Tests/SassAndCoffee.Core.Tests.csproj +++ b/SassAndCoffee.Core.Tests/SassAndCoffee.Core.Tests.csproj @@ -109,6 +109,7 @@ + diff --git a/SassAndCoffee.Core.Tests/SassFileCompilerTest.cs b/SassAndCoffee.Core.Tests/SassFileCompilerTest.cs index 6875ca2..dae61bd 100644 --- a/SassAndCoffee.Core.Tests/SassFileCompilerTest.cs +++ b/SassAndCoffee.Core.Tests/SassFileCompilerTest.cs @@ -31,15 +31,8 @@ string compileInput(string filename, string input) { var fixture = new SassFileCompiler(); - using(var of = File.CreateText(filename)) { - of.WriteLine(input); - } - try { - - // TODO: Fix this - // fixture.Init(TODO); - string result = fixture.ProcessFileContent(filename); + string result = fixture.ProcessFileContent(new TestCompilerFile(filename, input)); Console.WriteLine(result); return result; } finally { diff --git a/SassAndCoffee.Core.Tests/TestCompilerFile.cs b/SassAndCoffee.Core.Tests/TestCompilerFile.cs new file mode 100644 index 0000000..85794ac --- /dev/null +++ b/SassAndCoffee.Core.Tests/TestCompilerFile.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; + +namespace SassAndCoffee.Core.Tests { + internal class TestCompilerFile: ICompilerFile { + private readonly string _fileName; + private readonly string _content; + private readonly DateTime _lastWriteTimeUtc; + + public TestCompilerFile(string fileName, string content) { + this._fileName = fileName; + this._content = content; + _lastWriteTimeUtc = DateTime.UtcNow; + } + + public DateTime LastWriteTimeUtc { + get { + AssertFileExists(); + return _lastWriteTimeUtc; + } + } + + public Stream Open() + { + AssertFileExists(); + MemoryStream stream = new MemoryStream(); + StreamWriter writer = new StreamWriter(stream); + writer.Write(_content); + writer.Flush(); + stream.Seek(0, SeekOrigin.Begin); + return stream; + } + + private void AssertFileExists() + { + if (!Exists) { + throw new FileNotFoundException(); + } + } + + public string Name { + get { + return _fileName; + } + } + + public bool Exists { + get { + return _content != null; + } + } + + public ICompilerFile GetRelativeFile(string relativePath) + { + // not really canonicalizing the real path, but that's not needed here + return new TestCompilerFile(relativePath, null); + } + } +} diff --git a/SassAndCoffee.Core/Caching/FileCache.cs b/SassAndCoffee.Core/Caching/FileCache.cs index 0e562c6..74bfb99 100644 --- a/SassAndCoffee.Core/Caching/FileCache.cs +++ b/SassAndCoffee.Core/Caching/FileCache.cs @@ -19,10 +19,9 @@ public FileCache(string basePath) public CompilationResult GetOrAdd(string filename, Func compilationDelegate, string mimeType) { var outputFileName = Path.Combine(_basePath, filename); - FileInfo fi; + FileInfo fi = new FileInfo(outputFileName); - if (File.Exists(outputFileName)) { - fi = new FileInfo(outputFileName); + if (fi.Exists) { return new CompilationResult(true, File.ReadAllText(outputFileName), mimeType, fi.LastWriteTimeUtc); } @@ -32,7 +31,7 @@ public CompilationResult GetOrAdd(string filename, Func _engine; - public string[] InputFileExtensions { - get { return new[] { ".coffee" }; } + public IEnumerable InputFileExtensions { + get { yield return ".coffee"; } } public string OutputFileExtension { @@ -28,20 +31,15 @@ public string OutputMimeType { public CoffeeScriptFileCompiler(CoffeeScriptCompiler engine = null) { - _engine = engine; - } - - public void Init(ICompilerHost host) - { - _engine = _engine ?? new CoffeeScriptCompiler(); + _engine = new Lazy(() => engine ?? new CoffeeScriptCompiler()); } - public string ProcessFileContent(string inputFileContent) + public string ProcessFileContent(ICompilerFile inputFileContent) { - return _engine.Compile(File.ReadAllText(inputFileContent)); + return _engine.Value.Compile(inputFileContent.ReadAllText()); } - public string GetFileChangeToken(string inputFileContent) + public string GetFileChangeToken(ICompilerFile inputFileContent) { return ""; } diff --git a/SassAndCoffee.Core/Compilers/FileConcatenationCompiler.cs b/SassAndCoffee.Core/Compilers/FileConcatenationCompiler.cs index f1ef85c..045173c 100644 --- a/SassAndCoffee.Core/Compilers/FileConcatenationCompiler.cs +++ b/SassAndCoffee.Core/Compilers/FileConcatenationCompiler.cs @@ -1,3 +1,5 @@ +using SassAndCoffee.Core.Extensions; + namespace SassAndCoffee.Core.Compilers { using System; @@ -10,11 +12,12 @@ namespace SassAndCoffee.Core.Compilers public class FileConcatenationCompiler : ISimpleFileCompiler { - ICompilerHost _host; - IContentCompiler _compiler; + private static readonly Regex _lineRegex = new Regex(@"(?<=^\s*)(?!=\#|\s)((?!\.combined\s*$).)+?(?=\s*$)", RegexOptions.Compiled|RegexOptions.CultureInvariant|RegexOptions.ExplicitCapture|RegexOptions.IgnoreCase|RegexOptions.Singleline); + + private readonly IContentCompiler _compiler; - public string[] InputFileExtensions { - get { return new[] { ".combine" }; } + public IEnumerable InputFileExtensions { + get { yield return ".combine"; } } public string OutputFileExtension { @@ -25,62 +28,52 @@ public string OutputMimeType { get { return "text/javascript"; } } - static readonly Regex _commentRegex = new Regex("#.*$", RegexOptions.Compiled); - - public FileConcatenationCompiler(IContentCompiler compiler) + public FileConcatenationCompiler(IContentCompiler compiler) { + if (compiler == null) { + throw new ArgumentNullException("compiler"); + } _compiler = compiler; } - public void Init(ICompilerHost host) - { - _host = host; - } - - public string ProcessFileContent(string inputFileContent) + public string ProcessFileContent(ICompilerFile inputFileContent) { - var combineFileNames = this.GetCombineFileNames(inputFileContent); - - var allText = combineFileNames - .Select( x => _compiler.CanCompile(x) ? - _compiler.GetCompiledContent(x).Contents : String.Empty) - .ToArray(); - + IEnumerable combineFileNames = GetCombineFileNames(inputFileContent); + string[] allText = combineFileNames + .Select(x => _compiler.CanCompile(x) + ? _compiler.GetCompiledContent(x).Contents + : String.Empty) + .ToArray(); return allText.Aggregate(new StringBuilder(), (acc, x) => { - acc.Append(x); - acc.Append("\n"); - return acc; - }).ToString(); + acc.Append(x); + acc.Append("\n"); + return acc; + }).ToString(); } - public string GetFileChangeToken(string inputFileContent) + public string GetFileChangeToken(ICompilerFile inputFileContent) { - var md5sum = MD5.Create(); - - var ms = this.GetCombineFileNames(inputFileContent) - .Select(x => _compiler.GetSourceFileNameFromRequestedFileName(x)) - .Select(x => new FileInfo(x)) - .Where(x => x.Exists) - .Select(x => x.LastWriteTimeUtc.Ticks) - .Aggregate(new MemoryStream(), (acc, x) => { - var buf = BitConverter.GetBytes(x); - acc.Write(buf, 0, buf.Length); - return acc; - }); - + MD5 md5sum = MD5.Create(); + MemoryStream ms = GetCombineFileNames(inputFileContent) + .Select(x => _compiler.GetSourceFileNameFromRequestedFileName(x)) + .Where(x => (x != null) && x.Exists) + .Select(x => x.LastWriteTimeUtc.Ticks) + .Aggregate(new MemoryStream(), (acc, x) => { + byte[] buf = BitConverter.GetBytes(x); + acc.Write(buf, 0, buf.Length); + return acc; + }); return md5sum.ComputeHash(ms.GetBuffer()).Aggregate(new StringBuilder(), (acc, x) => { - acc.Append(x.ToString("x")); - return acc; - }).ToString(); + acc.Append(x.ToString("x")); + return acc; + }).ToString(); } - IEnumerable GetCombineFileNames(string inputFileContent) - { - return File.ReadAllLines(inputFileContent) - .Select(x => _commentRegex.Replace(x, String.Empty)) - .Where(x => !String.IsNullOrWhiteSpace(x)) - .Where(x => !x.ToLowerInvariant().EndsWith(".combine")) - .ToArray(); + private IEnumerable GetCombineFileNames(ICompilerFile inputFileContent) { + return inputFileContent.ReadLines() + .Select(l => _lineRegex.Match(l)) + .Where(m => m.Success) + .Select(m => inputFileContent.GetRelativeFile(m.Value)); } } } diff --git a/SassAndCoffee.Core/Compilers/ISimpleFileCompiler.cs b/SassAndCoffee.Core/Compilers/ISimpleFileCompiler.cs index cf67d13..b5b9626 100644 --- a/SassAndCoffee.Core/Compilers/ISimpleFileCompiler.cs +++ b/SassAndCoffee.Core/Compilers/ISimpleFileCompiler.cs @@ -1,14 +1,16 @@ +using System.Collections.Generic; +using System.IO; + namespace SassAndCoffee.Core.Compilers { // TODO: Document me public interface ISimpleFileCompiler { - string[] InputFileExtensions { get; } + IEnumerable InputFileExtensions { get; } string OutputFileExtension { get; } string OutputMimeType { get; } - void Init(ICompilerHost host); - string ProcessFileContent(string inputFileContent); - string GetFileChangeToken(string inputFileContent); + string ProcessFileContent(ICompilerFile inputFileContent); + string GetFileChangeToken(ICompilerFile inputFileContent); } } diff --git a/SassAndCoffee.Core/Compilers/JavascriptPassthroughCompiler.cs b/SassAndCoffee.Core/Compilers/JavascriptPassthroughCompiler.cs index f3b7ff8..00a98cb 100644 --- a/SassAndCoffee.Core/Compilers/JavascriptPassthroughCompiler.cs +++ b/SassAndCoffee.Core/Compilers/JavascriptPassthroughCompiler.cs @@ -1,3 +1,7 @@ +using System.Collections.Generic; + +using SassAndCoffee.Core.Extensions; + namespace SassAndCoffee.Core.Compilers { using System.IO; @@ -5,8 +9,10 @@ namespace SassAndCoffee.Core.Compilers // TODO: Document why this exists public class JavascriptPassthroughCompiler : ISimpleFileCompiler { - public string[] InputFileExtensions { - get { return new[] {".js"}; } + public IEnumerable InputFileExtensions { + get { + yield return ".js"; + } } public string OutputFileExtension { @@ -17,16 +23,12 @@ public string OutputMimeType { get { return "text/javascript"; } } - public void Init(ICompilerHost host) - { - } - - public string ProcessFileContent(string inputFileContent) + public string ProcessFileContent(ICompilerFile inputFileContent) { - return File.ReadAllText(inputFileContent); + return inputFileContent.ReadAllText(); } - public string GetFileChangeToken(string inputFileContent) + public string GetFileChangeToken(ICompilerFile inputFileContent) { return ""; } diff --git a/SassAndCoffee.Core/Compilers/MinifyingCompiler.cs b/SassAndCoffee.Core/Compilers/MinifyingCompiler.cs index 72c15f4..3fd7895 100644 --- a/SassAndCoffee.Core/Compilers/MinifyingCompiler.cs +++ b/SassAndCoffee.Core/Compilers/MinifyingCompiler.cs @@ -1,4 +1,9 @@ -namespace SassAndCoffee.Core.Compilers +using System; +using System.Collections.Generic; + +using SassAndCoffee.Core.Extensions; + +namespace SassAndCoffee.Core.Compilers { using System.IO; @@ -12,8 +17,11 @@ public class MinifyingFileCompiler : ISimpleFileCompiler TrashStack _coffeeEngine; TrashStack _engine; - public string[] InputFileExtensions { - get { return new[] {".js", ".coffee"}; } + public IEnumerable InputFileExtensions { + get { + yield return ".js"; + yield return ".coffee"; + } } public string OutputFileExtension { @@ -30,29 +38,22 @@ public MinifyingFileCompiler() _engine = new TrashStack(() => new MinifyingCompiler()); } - public void Init(ICompilerHost host) - { - } - - public string ProcessFileContent(string inputFileContent) + public string ProcessFileContent(ICompilerFile inputFileContent) { - string text = File.ReadAllText(inputFileContent); - - if (inputFileContent.ToLowerInvariant().EndsWith(".coffee")) { - using(var coffeeEngine = _coffeeEngine.Get()) { - text = coffeeEngine.Value.Compile(text); - } - } - - string ret; - using (var engine = _engine.Get()) { - ret = engine.Value.Compile(text); - } - - return ret; + string text = inputFileContent.ReadAllText(); + if (inputFileContent.Name.EndsWith(".coffee", StringComparison.OrdinalIgnoreCase)) { + using (ValueContainer coffeeEngine = _coffeeEngine.Get()) { + text = coffeeEngine.Value.Compile(text); + } + } + string ret; + using (ValueContainer engine = _engine.Get()) { + ret = engine.Value.Compile(text); + } + return ret; } - public string GetFileChangeToken(string inputFileContent) + public string GetFileChangeToken(ICompilerFile inputFileContent) { return ""; } diff --git a/SassAndCoffee.Core/Compilers/SassFileCompiler.cs b/SassAndCoffee.Core/Compilers/SassFileCompiler.cs index e23c8f3..c9af550 100644 --- a/SassAndCoffee.Core/Compilers/SassFileCompiler.cs +++ b/SassAndCoffee.Core/Compilers/SassFileCompiler.cs @@ -1,10 +1,14 @@ -namespace SassAndCoffee.Core.Compilers +using System.Diagnostics; +using System.Linq.Expressions; + +using SassAndCoffee.Core.Extensions; + +namespace SassAndCoffee.Core.Compilers { using System; using System.Collections.Generic; using System.IO; using System.Linq; - using System.Reflection; using IronRuby; @@ -19,39 +23,45 @@ private class SassModule public dynamic SassOption { get; set; } public dynamic ScssOption { get; set; } public Action ExecuteRubyCode { get; set; } + public VirtualFilePAL PlatformAdaptationLayer { get; set; } } static TrashStack _sassModule; internal static string RootAppPath; - ICompilerHost _compilerHost; static SassFileCompiler() { _sassModule = new TrashStack(() => { - var srs = new ScriptRuntimeSetup() {HostType = typeof (ResourceAwareScriptHost)}; + var srs = new ScriptRuntimeSetup() { + HostType = typeof (ResourceAwareScriptHost), + //DebugMode = Debugger.IsAttached + }; srs.AddRubySetup(); var runtime = Ruby.CreateRuntime(srs); var engine = runtime.GetRubyEngine(); // NB: 'R:\' is a garbage path that the PAL override below will // detect and attempt to find via an embedded Resource file - engine.SetSearchPaths(new List() {@"R:\lib\ironruby", @"R:\lib\ruby\1.9.1"}); - - var source = engine.CreateScriptSourceFromString(Utility.ResourceAsString("SassAndCoffee.Core.lib.sass_in_one.rb"), SourceCodeKind.File); + engine.SetSearchPaths(new[] {@"R:/lib/ironruby", @"R:/lib/ruby/1.9.1"}); + + var source = engine.CreateScriptSourceFromString(Utility.ResourceAsString("SassAndCoffee.Core.lib.sass_in_one.rb"), "R:/lib/sass_in_one.rb", SourceCodeKind.File); var scope = engine.CreateScope(); source.Execute(scope); - - return new SassModule() { + return new SassModule { + PlatformAdaptationLayer = (VirtualFilePAL)runtime.Host.PlatformAdaptationLayer, Engine = scope.Engine.Runtime.Globals.GetVariable("Sass"), - SassOption = engine.Execute("{:syntax => :sass}"), - ScssOption = engine.Execute("{:syntax => :scss}"), - ExecuteRubyCode = code => engine.Execute(code, scope), + SassOption = engine.Execute(@"{:syntax => :sass, :cache_location => ""C:/""}"), + ScssOption = engine.Execute(@"{:syntax => :scss, :cache_location => ""C:/""}"), + ExecuteRubyCode = code => engine.Execute(code, scope) }; }); } - public string[] InputFileExtensions { - get { return new[] {".scss", ".sass"}; } + public IEnumerable InputFileExtensions { + get { + yield return ".scss"; + yield return ".sass"; + } } public string OutputFileExtension { @@ -62,30 +72,17 @@ public string OutputMimeType { get { return "text/css"; } } - public void Init(ICompilerHost host) + public string ProcessFileContent(ICompilerFile inputFileContent) { - _compilerHost = host; - } - - public string ProcessFileContent(string inputFileContent) - { - // NB: We do this here instead of in Init like we should, because in - // ASP.NET trying to get the PhysicalAppPath when a request isn't in-flight - // is verboten, for no good reason. - RootAppPath = RootAppPath ?? _compilerHost.ApplicationBasePath; - using (var sassModule = _sassModule.Get()) { - dynamic opt = (inputFileContent.ToLowerInvariant().EndsWith("scss") ? sassModule.Value.ScssOption : sassModule.Value.SassOption); - - if (!inputFileContent.Contains('\'')) { - sassModule.Value.ExecuteRubyCode(String.Format("Dir.chdir '{0}'", Path.GetDirectoryName(inputFileContent))); + dynamic opt = (inputFileContent.Name.EndsWith(".scss", StringComparison.OrdinalIgnoreCase) ? sassModule.Value.ScssOption : sassModule.Value.SassOption); + using (sassModule.Value.PlatformAdaptationLayer.SetCompilerFile(inputFileContent)) { + return (string)sassModule.Value.Engine.compile(inputFileContent.ReadAllText(), opt); } - - return (string) sassModule.Value.Engine.compile(File.ReadAllText(inputFileContent), opt); } } - public string GetFileChangeToken(string inputFileContent) + public string GetFileChangeToken(ICompilerFile inputFileContent) { return ""; } @@ -93,54 +90,12 @@ public string GetFileChangeToken(string inputFileContent) public class ResourceAwareScriptHost : ScriptHost { - PlatformAdaptationLayer _innerPal = null; + private readonly PlatformAdaptationLayer _innerPal = new VirtualFilePAL(); + public override PlatformAdaptationLayer PlatformAdaptationLayer { get { - if (_innerPal == null) { - _innerPal = new ResourceAwarePAL(); - } return _innerPal; } } } - - public class ResourceAwarePAL : PlatformAdaptationLayer - { - public override Stream OpenInputFileStream(string path) - { - var ret = Assembly.GetExecutingAssembly().GetManifestResourceStream(pathToResourceName(path)); - if (ret != null) { - return ret; - } - - if (SassFileCompiler.RootAppPath == null || !path.ToLowerInvariant().StartsWith(SassFileCompiler.RootAppPath)) { - return null; - } - - return base.OpenInputFileStream(path); - } - - public override bool FileExists(string path) - { - if (Assembly.GetExecutingAssembly().GetManifestResourceInfo(pathToResourceName(path)) != null) { - return true; - } - - if (path.EndsWith("css")) { - int a = 1; - } - - return base.FileExists(path); - } - - string pathToResourceName(string path) - { - var ret = path - .Replace("1.9.1", "_1._9._1") - .Replace('\\', '.') - .Replace('/', '.') - .Replace("R:", "SassAndCoffee.Core"); - return ret; - } - } } diff --git a/SassAndCoffee.Core/Compilers/VirtualFilePAL.cs b/SassAndCoffee.Core/Compilers/VirtualFilePAL.cs new file mode 100644 index 0000000..1f3aab8 --- /dev/null +++ b/SassAndCoffee.Core/Compilers/VirtualFilePAL.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +using Microsoft.Scripting; + +namespace SassAndCoffee.Core.Compilers { + public class VirtualFilePAL: PlatformAdaptationLayer { + private enum PathType { + None, + Resource, + Virtual, + Cache + } + + private static readonly Regex _filenameHeuristic = new Regex(@"/[^./]+(\.[^.]+)+$", RegexOptions.Compiled|RegexOptions.CultureInvariant|RegexOptions.RightToLeft|RegexOptions.Singleline|RegexOptions.ExplicitCapture); + private static readonly Regex _pathCanonizer = new Regex(@"[/\\](\.(?=$|[/\\])|[^/\\]+[/\\]\.\.)", RegexOptions.CultureInvariant|RegexOptions.Compiled|RegexOptions.ExplicitCapture); + private static readonly Regex _resourcePathEscaper = new Regex(@"\b(?=\d)|[/\\]", RegexOptions.Compiled|RegexOptions.CultureInvariant|RegexOptions.ExplicitCapture); + private static readonly Regex _resourcePathTrimmer = new Regex(@"^.*(?=(\.[^.]+){2})", RegexOptions.Compiled|RegexOptions.CultureInvariant|RegexOptions.ExplicitCapture); + private static readonly Regex _pathAbsolute = new Regex(@"^((?[a-z]):)?[/\\]", RegexOptions.Compiled|RegexOptions.IgnoreCase|RegexOptions.CultureInvariant|RegexOptions.ExplicitCapture); +// private static readonly Regex _pathAnalyzer = new Regex(@"^(?(?([a-z]:)?[/\\])([^/\\]+[/\\])*)(?[^/\\]+)$", RegexOptions.Compiled|RegexOptions.CultureInvariant|RegexOptions.IgnoreCase|RegexOptions.ExplicitCapture); + private static readonly ICollection _resourceDirectories = new HashSet(typeof(VirtualFilePAL).Assembly.GetManifestResourceNames().Select(s => _resourcePathTrimmer.Match(s).Value)); + +/* internal static string JoinPaths(string basePath, string relativePath) { + Match relativeMatch = _pathAnalyzer.Match(relativePath); + if (relativeMatch.Groups["absolute"].Success) { + return CanonizePath(relativePath); + } + return CanonizePath(_pathAnalyzer.Match(basePath).Groups["path"].Value+relativePath); + } */ + + internal static string CanonizePath(string path) { + return _pathCanonizer.Replace(path, ""); + } + + private readonly string _cachePath; + private ICompilerFile _compilerFile; + private string _currentDirectory; + + public VirtualFilePAL() { + _cachePath = Path.Combine(Path.GetTempPath(), "sass_cache", Guid.NewGuid().ToString("N")); + } + + public override string CurrentDirectory { + get { + return _currentDirectory ?? @"C:\"; + } + set { + _currentDirectory = GetFullPath(value); + } + } + + internal static string GetDriveInternal(string path) { + return _pathAbsolute.Match(path).Groups["drive"].Value; + } + + public override void CreateDirectory(string path) { + switch (ResolvePath(ref path)) { + case PathType.Cache: + base.CreateDirectory(path); + break; + default: + throw new NotSupportedException(); + } + } + + public override void DeleteDirectory(string path, bool recursive) { + switch (ResolvePath(ref path)) { + case PathType.Cache: + base.DeleteDirectory(path, recursive); + break; + default: + throw new NotSupportedException(); + } + } + + public override void DeleteFile(string path, bool deleteReadOnly) { + switch (ResolvePath(ref path)) { + case PathType.Cache: + base.DeleteFile(path, deleteReadOnly); + break; + default: + throw new NotSupportedException(); + } + } + + public override bool DirectoryExists(string path) { + switch (ResolvePath(ref path)) { + case PathType.Resource: + return _resourceDirectories.Contains(path); + case PathType.Virtual: + return !(_filenameHeuristic.IsMatch(path) || _compilerFile.GetRelativeFile(path).Exists); + case PathType.Cache: + return base.DirectoryExists(path); + default: + throw new NotSupportedException(); + } + } + + public override bool FileExists(string path) { + switch (ResolvePath(ref path)) { + case PathType.Resource: + using (Stream stream = typeof(VirtualFilePAL).Assembly.GetManifestResourceStream(path)) { + return stream != null; + } + case PathType.Virtual: + return _compilerFile.GetRelativeFile(path).Exists; + case PathType.Cache: + return base.FileExists(path); + default: + throw new NotSupportedException(); + } + } + + public override string[] GetFileSystemEntries(string path, string searchPattern, bool includeFiles, bool includeDirectories) { + switch (ResolvePath(ref path)) { + case PathType.Cache: + return base.GetFileSystemEntries(path, searchPattern, includeFiles, includeDirectories); + default: + throw new NotSupportedException(); + } + } + + public override string GetFullPath(string path) { + Match match = _pathAbsolute.Match(path); + if (match.Success) { + if (!match.Groups["drive"].Success) { + path = GetDriveInternal(path)+':'+path; + } + } else { + path = CombinePaths(CurrentDirectory, path); + } + return CanonizePath(path); + } + + public override void MoveFileSystemEntry(string sourcePath, string destinationPath) { + if ((ResolvePath(ref sourcePath) == PathType.Cache) && (ResolvePath(ref destinationPath) == PathType.Cache)) { + base.MoveFileSystemEntry(sourcePath, destinationPath); + } + throw new NotSupportedException(); + } + + public override Stream OpenInputFileStream(string path) { + return OpenInputFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + } + + public override Stream OpenInputFileStream(string path, FileMode mode, FileAccess access, FileShare share) { + bool readOnly = (mode == FileMode.Open) && (access == FileAccess.Read); + switch (ResolvePath(ref path)) { + case PathType.Resource: + if (readOnly) { + return typeof(VirtualFilePAL).Assembly.GetManifestResourceStream(path); + } + break; + case PathType.Virtual: + if (readOnly) { + return _compilerFile.GetRelativeFile(path).Open(); + } + break; + case PathType.Cache: + return File.Open(Path.Combine(_cachePath, path), mode, access, share); + } + throw new NotSupportedException(); + } + + public override Stream OpenInputFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize) { + return OpenInputFileStream(path, mode, access, share); + } + + public override Stream OpenOutputFileStream(string path) { + switch (ResolvePath(ref path)) { + case PathType.Cache: + return base.OpenOutputFileStream(path); + default: + throw new NotSupportedException(); + } + } + + internal IDisposable SetCompilerFile(ICompilerFile file) { + if (file == null) { + throw new ArgumentNullException("file"); + } + Debug.Assert(_compilerFile == null); + _compilerFile = file; + _currentDirectory = Path.GetDirectoryName(@"V:"+file.Name); + Directory.CreateDirectory(_cachePath); + return Disposable.Create(delegate { + _compilerFile = null; + _currentDirectory = null; + Directory.Delete(_cachePath, true); + }); + } + + private PathType ResolvePath(ref string path) { + string[] parts = GetFullPath(path).Split(':'); + if (parts.Length == 2) { + path = parts[1]; + switch (parts[0]) { + case "R": + path = "SassAndCoffee.Core"+_resourcePathEscaper.Replace(parts[1], m => (m.Length == 0) ? "_" : "."); + return PathType.Resource; + case "V": + path = _compilerFile.GetRelativeFile(parts[1].Replace('\\', '/')).Name; + return PathType.Virtual; + case "C": + path = _cachePath+parts[1].Replace('/', '\\'); + return PathType.Cache; + } + } + return PathType.None; + } + } +} diff --git a/SassAndCoffee.Core/ContentCompiler.cs b/SassAndCoffee.Core/ContentCompiler.cs index dc75f37..d1d410e 100644 --- a/SassAndCoffee.Core/ContentCompiler.cs +++ b/SassAndCoffee.Core/ContentCompiler.cs @@ -11,15 +11,12 @@ public class ContentCompiler : IContentCompiler { - private readonly ICompilerHost _host; - private readonly ICompiledCache _cache; private readonly IEnumerable _compilers; - public ContentCompiler(ICompilerHost host, ICompiledCache cache) + public ContentCompiler(ICompiledCache cache) { - _host = host; _cache = cache; _compilers = new ISimpleFileCompiler[] { @@ -29,35 +26,28 @@ public ContentCompiler(ICompilerHost host, ICompiledCache cache) new SassFileCompiler(), new JavascriptPassthroughCompiler(), }; - - Init(); } - public ContentCompiler(ICompilerHost host, ICompiledCache cache, IEnumerable compilers) + public ContentCompiler(ICompiledCache cache, IEnumerable compilers) { - _host = host; _cache = cache; _compilers = compilers; - - Init(); } - public bool CanCompile(string requestedFileName) + public bool CanCompile(ICompilerFile physicalFileName) { - var physicalFileName = _host.MapPath(requestedFileName); - return _compilers.Any(x => physicalFileName.EndsWith(x.OutputFileExtension) && x.FindInputFileGivenOutput(physicalFileName) != null); + return GetMatchingCompiler(physicalFileName) != null; } - public CompilationResult GetCompiledContent (string requestedFileName) + public CompilationResult GetCompiledContent(ICompilerFile sourceFileName) { - var sourceFileName = _host.MapPath(requestedFileName); var compiler = GetMatchingCompiler(sourceFileName); if (compiler == null) { return CompilationResult.Error; } var physicalFileName = compiler.FindInputFileGivenOutput(sourceFileName); - if (!File.Exists(physicalFileName)) { + if (!physicalFileName.Exists) { return CompilationResult.Error; } @@ -65,18 +55,17 @@ public CompilationResult GetCompiledContent (string requestedFileName) return _cache.GetOrAdd(cacheKey, f => CompileContent(physicalFileName, compiler), compiler.OutputMimeType); } - public string GetSourceFileNameFromRequestedFileName(string requestedFileName) + public ICompilerFile GetSourceFileNameFromRequestedFileName(ICompilerFile physicalFileName) { - var physicalFileName = _host.MapPath(requestedFileName); var compiler = GetMatchingCompiler(physicalFileName); if (compiler == null) { - return string.Empty; + return null; } return compiler.FindInputFileGivenOutput(physicalFileName); } - public string GetOutputMimeType(string requestedFileName) + public string GetOutputMimeType(ICompilerFile requestedFileName) { var compiler = GetMatchingCompiler(requestedFileName); if (compiler == null) { @@ -86,33 +75,24 @@ public string GetOutputMimeType(string requestedFileName) return compiler.OutputMimeType; } - private string GetCacheKey(string physicalFileName, ISimpleFileCompiler compiler) + private string GetCacheKey(ICompilerFile physicalFileName, ISimpleFileCompiler compiler) { - var fi = new FileInfo(physicalFileName); var token = compiler.GetFileChangeToken(physicalFileName) ?? String.Empty; return String.Format("{0:yyyyMMddHHmmss}-{1}-{2}{3}", - fi.LastWriteTimeUtc, token, - Path.GetFileNameWithoutExtension(physicalFileName), + physicalFileName.LastWriteTimeUtc, token, + Path.GetFileNameWithoutExtension(physicalFileName.Name), compiler.OutputFileExtension); } - private CompilationResult CompileContent(string physicalFileName, ISimpleFileCompiler compiler) + private CompilationResult CompileContent(ICompilerFile physicalFileName, ISimpleFileCompiler compiler) { - var fi = new FileInfo(physicalFileName); - return new CompilationResult(true, compiler.ProcessFileContent(physicalFileName), compiler.OutputMimeType, fi.LastWriteTimeUtc); - } - - private void Init() - { - foreach (var simpleFileCompiler in _compilers) { - simpleFileCompiler.Init(_host); - } + return new CompilationResult(true, compiler.ProcessFileContent(physicalFileName), compiler.OutputMimeType, physicalFileName.LastWriteTimeUtc); } - private ISimpleFileCompiler GetMatchingCompiler(string physicalPath) + private ISimpleFileCompiler GetMatchingCompiler(ICompilerFile physicalPath) { - return _compilers.FirstOrDefault(x => physicalPath.EndsWith(x.OutputFileExtension) && x.FindInputFileGivenOutput(physicalPath) != null); + return _compilers.FirstOrDefault(x => physicalPath.Name.EndsWith(x.OutputFileExtension, StringComparison.OrdinalIgnoreCase) && x.FindInputFileGivenOutput(physicalPath) != null); } } } \ No newline at end of file diff --git a/SassAndCoffee.Core/Extensions/SimpleFileCompilerExtensions.cs b/SassAndCoffee.Core/Extensions/SimpleFileCompilerExtensions.cs index 103f72d..03f05ee 100644 --- a/SassAndCoffee.Core/Extensions/SimpleFileCompilerExtensions.cs +++ b/SassAndCoffee.Core/Extensions/SimpleFileCompilerExtensions.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; + namespace SassAndCoffee.Core.Extensions { using System.IO; @@ -6,20 +9,36 @@ namespace SassAndCoffee.Core.Extensions public static class SimpleFileCompilerExtensions { - public static string FindInputFileGivenOutput(this ISimpleFileCompiler This, string outputFilePath) - { - var rootFi = new FileInfo(outputFilePath); + public static ICompilerFile FindInputFileGivenOutput(this ISimpleFileCompiler This, ICompilerFile outputFilePath) { + var outputName = outputFilePath.Name; + if (outputName.EndsWith(This.OutputFileExtension, StringComparison.InvariantCultureIgnoreCase)) { + outputName = outputName.Substring(0, outputName.Length-This.OutputFileExtension.Length); + foreach (var ext in This.InputFileExtensions) { + ICompilerFile result = outputFilePath.GetRelativeFile(outputName+ext); + if (result.Exists) { + return result; + } + } + } + return null; + } - foreach (var ext in This.InputFileExtensions) { - var fi = new FileInfo(Path.Combine(rootFi.DirectoryName, - rootFi.FullName.ToLowerInvariant().Replace(This.OutputFileExtension, "") + ext)); + public static TextReader OpenReader(this ICompilerFile This) { + return new StreamReader(This.Open()); + } - if (fi.Exists) { - return fi.FullName; - } + public static string ReadAllText(this ICompilerFile This) { + using (var reader = This.OpenReader()) { + return reader.ReadToEnd(); } + } - return null; + public static IEnumerable ReadLines(this ICompilerFile This) { + using (var reader = This.OpenReader()) { + for (string line = reader.ReadLine(); line != null; line = reader.ReadLine()) { + yield return line; + } + } } } } diff --git a/SassAndCoffee.Core/ICompilerFile.cs b/SassAndCoffee.Core/ICompilerFile.cs new file mode 100644 index 0000000..b7e5bfc --- /dev/null +++ b/SassAndCoffee.Core/ICompilerFile.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; + +namespace SassAndCoffee.Core +{ + public interface ICompilerFile { + DateTime LastWriteTimeUtc { + get; + } + + Stream Open(); + + string Name { + get; + } + + bool Exists { + get; + } + + ICompilerFile GetRelativeFile(string relativePath); + } +} \ No newline at end of file diff --git a/SassAndCoffee.Core/ICompilerHost.cs b/SassAndCoffee.Core/ICompilerHost.cs deleted file mode 100644 index 2d93db8..0000000 --- a/SassAndCoffee.Core/ICompilerHost.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace SassAndCoffee.Core -{ - using SassAndCoffee.Core.Compilers; - - public interface ICompilerHost - { - /// - /// The base file system path for the application - /// - string ApplicationBasePath { get; } - - /// - /// Maps a url path to a physical file system path - /// - /// Url path - /// Absolute path to the file - string MapPath(string path); - } -} \ No newline at end of file diff --git a/SassAndCoffee.Core/IContentCompiler.cs b/SassAndCoffee.Core/IContentCompiler.cs index c37a47a..0bc0068 100644 --- a/SassAndCoffee.Core/IContentCompiler.cs +++ b/SassAndCoffee.Core/IContentCompiler.cs @@ -2,12 +2,12 @@ namespace SassAndCoffee.Core { public interface IContentCompiler { - bool CanCompile(string requestedFileName); + bool CanCompile(ICompilerFile requestedFileName); - CompilationResult GetCompiledContent (string requestedFileName); + CompilationResult GetCompiledContent(ICompilerFile requestedFileName); - string GetSourceFileNameFromRequestedFileName(string requestedFileName); + ICompilerFile GetSourceFileNameFromRequestedFileName(ICompilerFile requestedFileName); - string GetOutputMimeType(string requestedFileName); + string GetOutputMimeType(ICompilerFile requestedFileName); } } \ No newline at end of file diff --git a/SassAndCoffee.Core/JavascriptInterop.cs b/SassAndCoffee.Core/JavascriptInterop.cs index 17f1275..f678d0b 100644 --- a/SassAndCoffee.Core/JavascriptInterop.cs +++ b/SassAndCoffee.Core/JavascriptInterop.cs @@ -1,4 +1,6 @@ -namespace SassAndCoffee.Core +using System.Diagnostics; + +namespace SassAndCoffee.Core { using System; using System.Collections.Concurrent; @@ -136,8 +138,7 @@ public string Compile(string func, string code) public static class JS { - static Lazy _scriptCompilerImpl; - static object _gate = 42; + static readonly Lazy _scriptCompilerImpl; /* XXX: Why this crazy code is here * @@ -171,6 +172,7 @@ static JS() try { v8Assembly = Assembly.LoadFile(v8Name); } catch (Exception ex) { + Debug.WriteLine(ex); Console.Error.WriteLine("*** WARNING: You're on ARM, Mono, Itanium (heaven help you), or another architecture\n" + "which isn't x86/amd64 on NT. Loading the Jurassic compiler, which is much slower."); @@ -184,9 +186,7 @@ static JS() public static IV8ScriptCompiler CreateJavascriptCompiler() { - lock(_gate) { - return Activator.CreateInstance(_scriptCompilerImpl.Value) as IV8ScriptCompiler; - } + return Activator.CreateInstance(_scriptCompilerImpl.Value) as IV8ScriptCompiler; } } } diff --git a/SassAndCoffee.Core/SassAndCoffee.Core.csproj b/SassAndCoffee.Core/SassAndCoffee.Core.csproj index a8cad41..64b16e1 100644 --- a/SassAndCoffee.Core/SassAndCoffee.Core.csproj +++ b/SassAndCoffee.Core/SassAndCoffee.Core.csproj @@ -103,10 +103,11 @@ + - +