/// <summary> /// Crunched JS string passed to it, returning crunched string. /// The ErrorList property will be set with any errors found during the minification process. /// </summary> /// <param name="source">source Javascript</param> /// <param name="codeSettings">code minification settings</param> /// <returns>minified Javascript</returns> public string MinifyJavaScript(string source, CodeSettings codeSettings) { // default is an empty string var crunched = string.Empty; // reset the errors builder m_errorList = new List<ContextError>(); // create the parser from the source string. // pass null for the assumed globals array var parser = new JSParser(source); // file context is a property on the parser parser.FileContext = FileName; // hook the engine error event parser.CompilerError += OnJavaScriptError; try { if (codeSettings != null && codeSettings.PreprocessOnly) { // just run through the preprocessor only crunched = parser.PreprocessOnly(codeSettings); } else { // parse the input var scriptBlock = parser.Parse(codeSettings); if (scriptBlock != null) { // we'll return the crunched code if (codeSettings != null && codeSettings.Format == JavaScriptFormat.JSON) { // we're going to use a different output visitor -- one // that specifically returns valid JSON. var sb = new StringBuilder(); using (var stringWriter = new StringWriter(sb, CultureInfo.InvariantCulture)) { if (!JSONOutputVisitor.Apply(stringWriter, scriptBlock)) { m_errorList.Add(new ContextError( true, 0, null, null, null, this.FileName, 0, 0, 0, 0, JScript.InvalidJSONOutput)); } } crunched = sb.ToString(); } else { // just use the normal output visitor crunched = scriptBlock.ToCode(); } } } } catch (Exception e) { m_errorList.Add(new ContextError( true, 0, null, null, null, this.FileName, 0, 0, 0, 0, e.Message)); throw; } return crunched; }
private Parsed ParseFunction() { Parsed parsed = Parsed.False; if (CurrentTokenType == TokenType.Function) { var crunchedRGB = false; var functionText = GetRoot(CurrentTokenText); if (string.Compare(functionText, "rgb(", StringComparison.OrdinalIgnoreCase) == 0) { // rgb function parsing bool useRGB = false; // converting to #rrggbb or #rgb IF we don't find any significant comments! // skip any space or comments int[] rgb = new int[3]; // we're going to be building up the rgb function just in case we need it StringBuilder sbRGB = new StringBuilder(); sbRGB.Append(CurrentTokenText.ToLowerInvariant()); string comments = NextSignificantToken(); if (comments.Length > 0) { // add the comments sbRGB.Append(comments); // and signal that we need to use the RGB function because of them useRGB = true; } for (int ndx = 0; ndx < 3; ++ndx) { // if this isn't the first number, we better find a comma separator if (ndx > 0) { if (CurrentTokenType == TokenType.Character && CurrentTokenText == ",") { // add it to the rgb string builder sbRGB.Append(','); } else if (CurrentTokenType == TokenType.Character && CurrentTokenText == ")") { ReportError(0, CssErrorCode.ExpectedComma, CurrentTokenText); // closing paren is the end of the function! exit the loop useRGB = true; break; } else { ReportError(0, CssErrorCode.ExpectedComma, CurrentTokenText); sbRGB.Append(CurrentTokenText); useRGB = true; } // skip to the next significant comments = NextSignificantToken(); if (comments.Length > 0) { // add the comments sbRGB.Append(comments); // and signal that we need to use the RGB function because of them useRGB = true; } } // although we ALLOW negative numbers here, we'll trim them // later. But in the mean time, save a negation flag. bool negateNumber = false; if (CurrentTokenType == TokenType.Character && CurrentTokenText == "-") { negateNumber = true; comments = NextSignificantToken(); if (comments.Length > 0) { // add the comments sbRGB.Append(comments); // and signal that we need to use the RGB function because of them useRGB = true; } } // we might adjust the value, so save the token text string tokenText = CurrentTokenText; if (CurrentTokenType != TokenType.Number && CurrentTokenType != TokenType.Percentage) { ReportError(0, CssErrorCode.ExpectedRgbNumberOrPercentage, CurrentTokenText); useRGB = true; } else { if (CurrentTokenType == TokenType.Number) { // get the number value float numberValue; if (tokenText.TryParseSingleInvariant(out numberValue)) { numberValue *= (negateNumber ? -1 : 1); // make sure it's between 0 and 255 if (numberValue < 0) { tokenText = "0"; rgb[ndx] = 0; } else if (numberValue > 255) { tokenText = "255"; rgb[ndx] = 255; } else { rgb[ndx] = System.Convert.ToInt32(numberValue); } } else { // error -- not even a number. Keep the rgb function // (and don't change the token) useRGB = true; } } else { // percentage float percentageValue; if (tokenText.Substring(0, tokenText.Length - 1).TryParseSingleInvariant(out percentageValue)) { percentageValue *= (negateNumber ? -1 : 1); if (percentageValue < 0) { tokenText = "0%"; rgb[ndx] = 0; } else if (percentageValue > 100) { tokenText = "100%"; rgb[ndx] = 255; } else { rgb[ndx] = System.Convert.ToInt32(percentageValue * 255 / 100); } } else { // error -- not even a number. Keep the rgb function // (and don't change the token) useRGB = true; } } } // add the number to the rgb string builder sbRGB.Append(tokenText); // skip to the next significant comments = NextSignificantToken(); if (comments.Length > 0) { // add the comments sbRGB.Append(comments); // and signal that we need to use the RGB function because of them useRGB = true; } } if (useRGB) { // something prevented us from collapsing the rgb function // just output the rgb function we've been building up Append(sbRGB.ToString()); } else { // we can collapse it to either #rrggbb or #rgb // calculate the full hex string and crunch it string fullCode = "#{0:x2}{1:x2}{2:x2}".FormatInvariant(rgb[0], rgb[1], rgb[2]); string hexString = CrunchHexColor(fullCode, Settings.ColorNames, m_noColorAbbreviation); Append(hexString); // set the flag so we know we don't want to add the closing paren crunchedRGB = true; } } else if (string.Compare(functionText, "expression(", StringComparison.OrdinalIgnoreCase) == 0) { Append(CurrentTokenText.ToLowerInvariant()); NextToken(); // for now, just echo out everything up to the matching closing paren, // taking into account that there will probably be other nested paren pairs. // The content of the expression is JavaScript, so we'd really // need a full-blown JS-parser to crunch it properly. Kinda scary. // Start the parenLevel at 0 because the "expression(" token contains the first paren. var jsBuilder = new StringBuilder(); int parenLevel = 0; while (!m_scanner.EndOfFile && (CurrentTokenType != TokenType.Character || CurrentTokenText != ")" || parenLevel > 0)) { if (CurrentTokenType == TokenType.Function) { // the function token INCLUDES the opening parenthesis, // so up the paren level whenever we find a function. // AND this includes the actual expression( token -- so we'll // hit this branch at the beginning. Make sure the parenLevel // is initialized to take that into account ++parenLevel; } else if (CurrentTokenType == TokenType.Character) { switch (CurrentTokenText) { case "(": // start a nested paren ++parenLevel; break; case ")": // end a nested paren // (we know it's nested because if it wasn't, we wouldn't // have entered the loop) --parenLevel; break; } } jsBuilder.Append(CurrentTokenText); NextToken(); } // create a JSParser object with the source we found, crunch it, and send // the minified script to the output var expressionCode = jsBuilder.ToString(); if (Settings.MinifyExpressions) { // we want to minify the javascript expressions. // create a JSParser object from the code we parsed. JSParser jsParser = new JSParser(expressionCode); // copy the file context jsParser.FileContext = this.FileContext; // hook the error handler and set the "contains errors" flag to false. // the handler will set the value to true if it encounters any errors var containsErrors = false; jsParser.CompilerError += (sender, ea) => { ReportError(0, CssErrorCode.ExpressionError, ea.Error.Message); containsErrors = true; }; // parse the source as an expression using our common JS settings Block block = jsParser.Parse(m_jsSettings); // if we got back a parsed block and there were no errors, output the minified code. // if we didn't get back the block, or if there were any errors at all, just output // the raw expression source. if (block != null && !containsErrors) { Append(block.ToCode()); } else { Append(expressionCode); } } else { // we don't want to minify expression code for some reason. // just output the code exactly as we parsed it Append(expressionCode); } } else if (string.Compare(functionText, "calc(", StringComparison.OrdinalIgnoreCase) == 0) { Append(CurrentTokenText.ToLowerInvariant()); SkipSpace(); // one sum parsed = ParseSum(); } else if (string.Compare(functionText, "min(", StringComparison.OrdinalIgnoreCase) == 0 || string.Compare(functionText, "max(", StringComparison.OrdinalIgnoreCase) == 0) { Append(CurrentTokenText.ToLowerInvariant()); SkipSpace(); // need to be one or more sums, separated by commas // (ParseSum will only return true or false -- never empty) parsed = ParseSum(); while (parsed == Parsed.True && CurrentTokenType == TokenType.Character && CurrentTokenText == ",") { AppendCurrent(); SkipSpace(); parsed = ParseSum(); } } else { // generic function parsing AppendCurrent(); SkipSpace(); if (ParseFunctionParameters() == Parsed.False) { ReportError(0, CssErrorCode.ExpectedExpression, CurrentTokenText); } } if (CurrentTokenType == TokenType.Character && CurrentTokenText == ")") { if (!crunchedRGB) { Append(')'); } SkipSpace(); parsed = Parsed.True; } else { ReportError(0, CssErrorCode.UnexpectedToken, CurrentTokenText); } } return parsed; }