How can I output errors when using .less programmatically?
Asked Answered
P

7

11

I've written an ASP.NET MVC action method that receives a .less file name, processes it via Less.Parse(<filename>) and outputs the processed css file.

This works fine as long as the .less code is valid, but if there is an error, dotLess just returns an empty string. So if there is an error processing the file, my action method returns an empty css file.

How can I output an error message with a closer description of the syntax error instead?

Pantaloon answered 25/1, 2011 at 19:53 Comment(7)
Have you seen SquishIt? codethinked.com/post/2010/05/26/…Cidevant
@qstarin: Thanks, but I am already using combres ( combres.codeplex.com ) for production purposes and combres can do everything squishit does (plus quite a bit more). But I still need the answer to my question above for development purposes. The reason is that I don't like working with a combined download include of all of my CSS files, instead I prefer separate files. And everything works fine, except for the error messages...Pantaloon
I'm curious, what useful features does combres provide that SquishIt does not? Looking through Combres's documentation it appears they do much the same tasks. SquishIt does, however, include dotLess into it's processing. That is why I mentioned it, because you would not need a separate action to perform the dotLess compilation.Cidevant
And a peek at dotLess's source shows a way to get error info, I will write up an answer and post it in a moment.Cidevant
@qstarin: Combres also comes with dotless support, but if I understand things correctly, neither combres nor squishit allow me to output my css files separately in debug mode. Plus, I also want to reference dotless variables and mixins from one global file within all other files, even when not combining everything into one file. That's what I coded my own workaround for.Pantaloon
ah, I use a single .less file that imports all of my other .less files to handle the variables and mixins issue.Cidevant
@qstarin: For us this would not be a good option since we are developing a quite big site with lots and lots of css stylesheets. Having everything squished together would harder to debug.Pantaloon
C
14

The dotLess parser traps Exceptions and outputs them to a Logger. The snippet from dotLess's source that performs this is LessEngine.TransformToCss:

public string TransformToCss(string source, string fileName)
{
    try
    {
        Ruleset ruleset = this.Parser.Parse(source, fileName);
        Env env = new Env();
        env.Compress = this.Compress;
        Env env2 = env;
        return ruleset.ToCSS(env2);
    }
    catch (ParserException exception)
    {
        this.Logger.Error(exception.Message);
    }
    return "";
}

Less.Parse has an overload that takes a DotlessConfiguration object, which provides several properties that you can use:

public class DotlessConfiguration
{
    // Properties
    public bool CacheEnabled { get; set; }
    public Type LessSource { get; set; }
    public Type Logger { get; set; }
    public LogLevel LogLevel { get; set; }
    public bool MinifyOutput { get; set; }
    public int Optimization { get; set; }
    public bool Web { get; set; }
}

You will notice that the Logger property is of type Type. Whatever type you supply must implement dotless.Core.Loggers.ILogger:

public interface ILogger
{
    // Methods
    void Debug(string message);
    void Error(string message);
    void Info(string message);
    void Log(LogLevel level, string message);
    void Warn(string message);
}

As we saw in the first snippet, the Error method on the logger will get called when an error is encountered during parsing.

Now, the one sticky point of all this is how exactly an instance of the type that implements ILogger gets instantiated. Internally, dotLess uses an IoC container that is baked into the DLL. Following the method calls, it appears that it will eventually call Activator.CreateInstance to instantiate your ILogger.

I hope this is at least somewhat helpful.

Cidevant answered 25/1, 2011 at 23:32 Comment(4)
I really should rework the logger API a bit.. At the time of writing it made sense with only the internal usages in mind.. but for external callers this is pretty bad..Buddie
@Tigraine: I hope at least I've explained correctly how to catch logging events, I was a bit unsure if I'd gotten it all down, particularly what the ramifications were of specifying a DotlessConfiguration object with only the logging class filled in. Also, if you are reworking this, I'd point to SimpleLoggingFramework or Common.Logging as two potential facades to logging systems, though honestly it can get to be mess with all the dependencies .... good luck. hehCidevant
Thanks.. We try to avoid external dependencies like the pest that's why we didn't already take a dependency on log4net or something but pretty much copied the Castle.ILogger idea.. But we can at least make it easier to plug in your own logging methods more easily..Buddie
Perfect +1... helped me find that the dotless parser chokes on two ";;" in a row in css! Now happily bundling minimised dotless css :)Pilaf
T
5

I just faced this today in my RequestReduce project. I was getting blank less -> css transforms because there were parse errors that appeared to be going into the ether. Thanks to qes's answer I was able to work out a solution where I could write the errors to the response stream. Here is my dotless.Core.Loggers.ILogger:

public class LessLogger : ILogger
{
    public void Log(LogLevel level, string message)
    {
    }

    public void Info(string message)
    {
    }

    public void Debug(string message)
    {
    }

    public void Warn(string message)
    {
    }

    public void Error(string message)
    {
        Response.Write(message);
    }

    public HttpResponseBase Response { get; set; }
}

I pass this into the Configuration sent to the EngineFactory:

            var engine = new EngineFactory(new DotlessConfiguration
                                               {
                                                   CacheEnabled = false,
                                                   Logger = typeof (LessLogger)
                                               }
                ).GetEngine();

For unit testing purposes I wanted to pass in my HttpResponseBase that would write the error. This is where I felt things getting ugly with some nasty casting to get a reference to my logger:

            ((LessLogger)((LessEngine)((ParameterDecorator)engine).Underlying).Logger).Response = response;

I hope this helps out and if someone knows of a more elegant way to get a reference to the logger, please let me know.

Truditrudie answered 9/12, 2011 at 17:36 Comment(1)
Where do you put the LessLogger class?Kerchief
E
5

You can do this very easily with web.config. In your dotless configuration section, add the following: logger="dotless.Core.Loggers.AspResponseLogger". This will make dotless output the errors instead of blank css.

I've included the following as an example. ("..." represents existing stuff in your web.config). In my example below cache is set to false. This is useful for debugging purposes. It should probably be set to true under normal circumstances.

<configuration>    
     <configSections>
           ...
          <section name="dotless" type="dotless.Core.configuration.DotlessConfigurationSectionHandler,dotless.Core" />
      </configSections>

      <dotless minifyCss="false" cache="false" 
            logger="dotless.Core.Loggers.AspResponseLogger" />
       ...    
</configuration>    
Epimorphosis answered 17/5, 2012 at 19:21 Comment(2)
I've added the line which looks as follows <dotless minifyCss="false" cache="false" web="false" logger="dotless.Core.Loggers.AspResponseLogger" /> Does nothing for me?Grodin
+1 This works - if using something like SquishIt, make sure you just reference the less files directly in your browser (or whatever) to view the errorsQuimby
M
1

I am using a wrapper class around dotless, as follows:

public class LessParser : IStylizer
{
    public string ErrorFileName { get; private set; }
    public int ErrorLineNumber { get; private set; }
    public int ErrorPosition { get; private set; }
    public string ErrorMessage { get; private set; }

    string IStylizer.Stylize(Zone zone)
    {
        ErrorFileName = zone.FileName;
        ErrorLineNumber = zone.LineNumber;
        ErrorPosition = zone.Position;
        ErrorMessage = zone.Message;

        return String.Empty;
    }

    public string Compile(string lessContent, string lessPath)
    {
        var lessEngine = new EngineFactory(new DotlessConfiguration
        {
            CacheEnabled = false,
            DisableParameters = true,
            LogLevel = LogLevel.Error,
            MinifyOutput = true
        }).GetEngine();

        lessEngine.CurrentDirectory = lessPath;

        /* uncomment if DisableParameters is false
        if (lessEngine is ParameterDecorator)
            lessEngine = ((ParameterDecorator)lessEngine).Underlying;
        */

        /* uncomment if CacheEnabled is true
        if (lessEngine is CacheDecorator)
            lessEngine = ((CacheDecorator)lessEngine).Underlying;
        */

        ((LessEngine)lessEngine).Parser.Stylizer = this;

        return lessEngine.TransformToCss(lessContent, null);
    }

    public FileInfo SyncCss(FileInfo lessFile)
    {
        var cssFile = new FileInfo(
            lessFile.FullName.Substring(0, lessFile.FullName.Length - lessFile.Extension.Length) + ".css");

        if (!cssFile.Exists || cssFile.LastWriteTimeUtc < lessFile.LastWriteTimeUtc)
        {
            string cssContent = Compile(ReadFileContent(lessFile), lessFile.DirectoryName);

            if (String.IsNullOrEmpty(cssContent))
                return null;

            using (var stream = cssFile.Open(FileMode.Create))
            using (var writer = new StreamWriter(stream, Encoding.UTF8))
            {
                writer.Write(cssContent);
            }
        }

        return cssFile;
    }

    public string ReadFileContent(FileInfo file)
    {
        using (var reader = file.OpenText())
        {
            return reader.ReadToEnd();
        }
    }
}

The trick is to use own implementation of IStylizer interface that is called upon encountering a parse error to format the resulting error message. This allows us to capture discrete pieces of the error, unlike implementation of ILogger interface where the error is already a formatted text.

var parser = new LessParser();
var lessFile = new FileInfo("C:\\temp\\sample.less"));
var cssFile = parser.SyncCss(lessFile);

if (cssFile != null)
    Console.WriteLine(parser.ReadFileContent(cssFile));
else
    Console.WriteLine("Error '{3}' in {0}, line {1}, position {2}",
        parser.ErrorFileName, parser.ErrorLineNumber, parser.ErrorPosition, parser.ErrorMessage);
Mufi answered 4/5, 2015 at 14:6 Comment(0)
K
0

For the benefit of others, @tony722's solution works if you simply reference .less files from your pages.

But if you call Less.Parse directly, this method will write any error into Response:

var lessConfig = new DotlessConfiguration { Logger = typeof(AspResponseLogger) };
string css = Less.Parse(someInput, lessConfig);
Krenek answered 11/4, 2013 at 2:47 Comment(0)
S
0

This logs to output window in VS:

var config = dotless.Core.configuration.DotlessConfiguration.GetDefault();
config.Logger = new dotless.Core.Loggers.DiagnosticsLogger(dotless.Core.Loggers.LogLevel.Debug).GetType();
config.MinifyOutput = minified;
css= Less.Parse(css, config);
Slickenside answered 25/3, 2015 at 11:36 Comment(1)
is this solution on it's own. It does it require a logger to be implemented like the prior ones?Korean
M
0

This will log any LESS parsing errors to the debug console:

var config = dotless.Core.configuration.DotlessConfiguration.GetDefault();
config.Logger = new dotless.Core.Loggers.ConsoleLogger(dotless.Core.Loggers.LogLevel.Debug).GetType();

var lessCSS = Less.Parse("your css source", config);
Maieutic answered 5/8, 2022 at 22:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.