private int ProcessCrunchGroup(CrunchGroup crunchGroup) { int retVal = 0; // length of all the source files combined long sourceLength = 0; // create a string builder we'll dump our output into StringBuilder outputBuilder = new StringBuilder(); try { // if we have a resource file, process it now ResourceStrings resourceStrings = null; if (crunchGroup.Resource.Length > 0) { try { resourceStrings = ProcessResourceFile(crunchGroup.Resource); } catch (IOException e) { Debug.WriteLine(e.ToString()); } catch (BadImageFormatException e) { Debug.WriteLine(e.ToString()); } } switch (crunchGroup.InputType) { case InputType.Css: // see how many input files there are if (crunchGroup.Count == 0) { // no input files -- take from stdin retVal = ProcessCssFile(string.Empty, m_encodingInputName, resourceStrings, outputBuilder, ref sourceLength); } else { // process each input file for (int ndx = 0; retVal == 0 && ndx < crunchGroup.Count; ++ndx) { retVal = ProcessCssFile(crunchGroup[ndx], crunchGroup[ndx].EncodingName ?? m_encodingInputName, resourceStrings, outputBuilder, ref sourceLength); } } break; case InputType.JavaScript: if (resourceStrings != null) { // make sure the resource strings know the name of the resource object resourceStrings.Name = crunchGroup.ResourceObjectName; } if (m_echoInput && resourceStrings != null) { // we're just echoing the output -- so output a JS version of the dictionary // create JS from the dictionary and output it to the stream // leave the object null string resourceObject = CreateJSFromResourceStrings(resourceStrings); outputBuilder.Append(resourceObject); // just add the number of characters to the length // it's just an approximation // NO! We don't want to include this code in the calculations. // it's not actually part of the sources //sourceLength += resourceObject.Length; } // process each input file // we'll keep track of whether the last file ended in a semi-colon. // we start with true so we don't add one before the first block bool lastEndedSemiColon = true; try { // see how many input files there are if (crunchGroup.Count == 0) { // take input from stdin retVal = ProcessJSFile(string.Empty, m_encodingInputName, resourceStrings, outputBuilder, ref lastEndedSemiColon, ref sourceLength); } else { // process each input file in turn for (int ndx = 0; retVal == 0 && ndx < crunchGroup.Count; ++ndx) { retVal = ProcessJSFile(crunchGroup[ndx], crunchGroup[ndx].EncodingName ?? m_encodingInputName, resourceStrings, outputBuilder, ref lastEndedSemiColon, ref sourceLength); } } } catch (JScriptException e) { retVal = 1; System.Diagnostics.Debug.WriteLine(e.ToString()); WriteError(string.Format(CultureInfo.InvariantCulture, "JS{0}", (e.Error & 0xffff)), e.Message); } // if we want to ensure the stream ends in a semi-colon (command-line option) // and the last file didn't... if (m_terminateWithSemicolon && !lastEndedSemiColon) { // add one now outputBuilder.Append(';'); } break; default: throw new UsageException(m_outputMode, "ConflictingInputType"); } // if we are pretty-printing, add a newline if (m_prettyPrint) { outputBuilder.AppendLine(); } } catch (Exception e) { retVal = 1; System.Diagnostics.Debug.WriteLine(e.ToString()); WriteError("AM-EXCEPTION", e.Message); } string crunchedCode = outputBuilder.ToString(); Encoding encodingOutput = GetOutputEncoding(crunchGroup.InputType, crunchGroup.Output.EncodingName); // now write the final output file if (string.IsNullOrEmpty(crunchGroup.Output)) { // no output file specified - send to STDOUT // if the code is empty, don't bother outputting it to the console if (!string.IsNullOrEmpty(crunchedCode)) { // set the console encoding try { // try setting the appropriate output encoding Console.OutputEncoding = encodingOutput; } catch (IOException e) { // sometimes they will error, in which case we'll just set it to ascii Debug.WriteLine(e.ToString()); Console.OutputEncoding = Encoding.ASCII; } // however, for some reason when I set the output encoding it // STILL doesn't call the EncoderFallback to Unicode-escape characters // not supported by the encoding scheme. So instead we need to run the // translation outselves. Still need to set the output encoding, though, // so the translated bytes get displayed properly in the console. byte[] encodedBytes = encodingOutput.GetBytes(crunchedCode); // only output the size analysis if we are in analyze mode // change: no, output the size analysis all the time. // (unless in silent mode, but WriteProgess will take care of that) ////if (m_analyze) { // output blank line before WriteProgress(); // if we are echoing the input, don't bother reporting the // minify savings because we don't have the minified output -- // we have the original output double percentage; if (!m_echoInput) { // calculate the percentage saved percentage = Math.Round((1 - ((double) encodedBytes.Length)/sourceLength)*100, 1); WriteProgress(StringMgr.GetString( "SavingsMessage", sourceLength, encodedBytes.Length, percentage )); } else { WriteProgress(StringMgr.GetString( "SavingsOutputMessage", encodedBytes.Length )); } // calculate how much a simple gzip compression would compress the output long gzipLength = CalculateGzipSize(encodedBytes); // calculate the savings and display the result percentage = Math.Round((1 - ((double)gzipLength) / encodedBytes.Length) * 100, 1); WriteProgress(StringMgr.GetString("SavingsGzipMessage", gzipLength, percentage)); // blank line after WriteProgress(); } // send to console out Console.Out.Write(Console.OutputEncoding.GetChars(encodedBytes)); //Console.Out.Write(crunchedCode); } } else { // send output to file try { // make sure the destination folder exists FileInfo fileInfo = new FileInfo(crunchGroup.Output); DirectoryInfo destFolder = new DirectoryInfo(fileInfo.DirectoryName); if (!destFolder.Exists) { destFolder.Create(); } if (!File.Exists(crunchGroup.Output) || m_clobber) { if (m_clobber && File.Exists(crunchGroup.Output) && (File.GetAttributes(crunchGroup.Output) & FileAttributes.ReadOnly) != 0) { // the file exists, we said we want to clobber it, but it's marked as // read-only. Reset that flag. File.SetAttributes( crunchGroup.Output, (File.GetAttributes(crunchGroup.Output) & ~FileAttributes.ReadOnly) ); } // create the output file using the given encoding using (StreamWriter outputStream = new StreamWriter( crunchGroup.Output, false, encodingOutput )) { outputStream.Write(crunchedCode); } // only output the size analysis if there is actually some output to measure if (File.Exists(crunchGroup.Output)) { // get the size of the resulting file FileInfo crunchedFileInfo = new FileInfo(crunchGroup.Output); long crunchedLength = crunchedFileInfo.Length; if (crunchedLength > 0) { // blank line before WriteProgress(); // if we are just echoing the input, don't bother calculating // the minify savings because there aren't any double percentage; if (!m_echoInput) { // calculate the percentage saved by minification percentage = Math.Round((1 - ((double) crunchedLength)/sourceLength)*100, 1); WriteProgress(StringMgr.GetString( "SavingsMessage", sourceLength, crunchedLength, percentage )); } else { WriteProgress(StringMgr.GetString( "SavingsOutputMessage", crunchedLength )); } // compute how long a simple gzip might compress the resulting file long gzipLength = CalculateGzipSize(File.ReadAllBytes(crunchGroup.Output)); // calculate the percentage of compression and display the results percentage = Math.Round((1 - ((double) gzipLength)/crunchedLength)*100, 1); WriteProgress(StringMgr.GetString("SavingsGzipMessage", gzipLength, percentage)); // blank line after WriteProgress(); } } } else { retVal = 1; WriteError("AM-IO", StringMgr.GetString("NoClobberError", crunchGroup.Output)); } } catch (ArgumentException e) { retVal = 1; System.Diagnostics.Debug.WriteLine(e.ToString()); WriteError("AM-PATH", e.Message); } catch (UnauthorizedAccessException e) { retVal = 1; System.Diagnostics.Debug.WriteLine(e.ToString()); WriteError("AM-AUTH", e.Message); } catch (PathTooLongException e) { retVal = 1; System.Diagnostics.Debug.WriteLine(e.ToString()); WriteError("AM-PATH", e.Message); } catch (SecurityException e) { retVal = 1; System.Diagnostics.Debug.WriteLine(e.ToString()); WriteError("AM-SEC", e.Message); } catch (IOException e) { retVal = 1; System.Diagnostics.Debug.WriteLine(e.ToString()); WriteError("AM-IO", e.Message); } } if (retVal == 0 && m_errorsFound) { // make sure we report an error retVal = 1; } return retVal; }
private static CrunchGroup[] ProcessXmlFile(string xmlPath, string resourcePath, string resourceObjectName, string outputFolder) { // list of crunch groups we're going to create by reading the XML file List<CrunchGroup> crunchGroups = new List<CrunchGroup>(); try { // save the XML file's directory name because we'll use it as a root // for all the other paths in the file string rootPath = Path.GetDirectoryName(xmlPath); // open the xml file XmlDocument xmlDoc = new XmlDocument(); xmlDoc.Load(xmlPath); // get a list of all <output> nodes XmlNodeList outputNodes = xmlDoc.SelectNodes("//output"); if (outputNodes.Count > 0) { // process each <output> node for (int ndxOutput = 0; ndxOutput < outputNodes.Count; ++ndxOutput) { // shortcut XmlNode outputNode = outputNodes[ndxOutput]; // get the output file path from the path attribute (if any) // it's okay for ther eto be no output file; if that's the case, // the output is sent to the STDOUT stream XmlAttribute pathAttribute = outputNode.Attributes["path"]; string outputPath = (pathAttribute == null ? string.Empty : pathAttribute.Value); // if we have a value and it's a relative path... if (outputPath.Length > 0 && !Path.IsPathRooted(outputPath)) { if (string.IsNullOrEmpty(outputFolder)) { // make it relative to the XML file outputPath = Path.Combine(rootPath, outputPath); } else { // make it relative to the output folder outputPath = Path.Combine(outputFolder, outputPath); } } // see if an encoding override has been specified var encodingAttribute = outputNode.Attributes["encoding"]; var encodingOutputName = encodingAttribute != null ? encodingAttribute.Value : null; // create the crunch group CrunchGroup crunchGroup = new CrunchGroup(outputPath, encodingOutputName); // see if there's an explicit input type, and if so, set the crunch group type var typeAttribute = outputNode.Attributes["type"]; if (typeAttribute != null) { switch (typeAttribute.Value.ToUpperInvariant()) { case "JS": case "JAVASCRIPT": case "JSCRIPT": crunchGroup.InputType = InputType.JavaScript; break; case "CSS": case "STYLESHEET": case "STYLESHEETS": crunchGroup.InputType = InputType.Css; break; } } // see if there is a resource node XmlNode resourceNode = outputNode.SelectSingleNode("./resource"); if (resourceNode != null) { // the path attribute MUST exist, or we will throw an error pathAttribute = resourceNode.Attributes["path"]; if (pathAttribute != null) { // get the value from the attribute string resourceFile = pathAttribute.Value; // if it's a relative path... if (!Path.IsPathRooted(resourceFile)) { // make it relative from the XML file resourceFile = Path.Combine(rootPath, resourceFile); } // make sure the resource file actually exists! It's an error if it doesn't. if (!File.Exists(resourceFile)) { throw new XmlException(StringMgr.GetString( "XmlResourceNotExist", pathAttribute.Value )); } // add it to the group crunchGroup.Resource = resourceFile; } else { throw new XmlException(StringMgr.GetString("ResourceNoPathAttr")); } // if there is a name attribute, we will use it for the object name XmlAttribute nameAttribute = resourceNode.Attributes["name"]; if (nameAttribute != null) { // but first make sure it isn't empty string objectName = nameAttribute.Value; if (!string.IsNullOrEmpty(objectName)) { crunchGroup.ResourceObjectName = objectName; } } // if no name was specified, use our default name if (string.IsNullOrEmpty(crunchGroup.ResourceObjectName)) { crunchGroup.ResourceObjectName = c_defaultResourceObjectName; } } else if (!string.IsNullOrEmpty(resourcePath)) { // just use the global resource path and object name passed to us // (if anything was passed at all) crunchGroup.Resource = resourcePath; crunchGroup.ResourceObjectName = resourceObjectName; } // get a list of <input> nodes XmlNodeList inputNodes = outputNode.SelectNodes("./input"); if (inputNodes.Count > 0) { // for each <input> element under the <output> node for (int ndxInput = 0; ndxInput < inputNodes.Count; ++ndxInput) { // add the path attribute value to the string list. // the path attribute MUST exist, or we will throw an error pathAttribute = inputNodes[ndxInput].Attributes["path"]; if (pathAttribute != null) { // get the value from the attribute string inputFile = pathAttribute.Value; // if it's a relative path... if (!Path.IsPathRooted(inputFile)) { // make it relative from the XML file inputFile = Path.Combine(rootPath, inputFile); } // make sure the input file actually exists! It's an error if it doesn't. if (!File.Exists(inputFile)) { throw new XmlException(StringMgr.GetString( "XmlInputNotExist", pathAttribute.Value )); } // if we don't know the type yet, let's see if the extension gives us a hint if (crunchGroup.InputType == InputType.Unknown) { switch (Path.GetExtension(inputFile).ToUpperInvariant()) { case ".JS": crunchGroup.InputType = InputType.JavaScript; break; case ".CSS": crunchGroup.InputType = InputType.Css; break; } } // see if there is an encoding attribute encodingAttribute = inputNodes[ndxInput].Attributes["encoding"]; string encodingName = encodingAttribute != null ? encodingAttribute.Value : null; // add the input file and its encoding (if any) to the group crunchGroup.Add(inputFile, encodingName); } else { // no required path attribute on the <input> element throw new XmlException(StringMgr.GetString("InputNoPathAttr")); } } // add the crunch group to the list crunchGroups.Add(crunchGroup); } else { // no required <input> nodes inside the <output> node throw new XmlException(StringMgr.GetString("OutputNoInputNodes")); } } } else { // no required <output> nodes // throw an error to end all processing throw new UsageException(ConsoleOutputMode.Console, "XmlNoOutputNodes"); } } catch (XmlException e) { // throw an error indicating the XML error System.Diagnostics.Debug.WriteLine(e.ToString()); throw new UsageException(ConsoleOutputMode.Console, "InputXmlError", e.Message); } // return an array of CrunchGroup objects return crunchGroups.ToArray(); }
private int Run() { int retVal = 0; CrunchGroup[] crunchGroups; // see if we have an XML file to process if (!string.IsNullOrEmpty(m_xmlInputFile)) { // process the XML file, using the output path as an optional output root folder crunchGroups = ProcessXmlFile(m_xmlInputFile, m_resourceFile, m_resourceObjectName, m_outputFile); } else { // just pass the input and output files specified in the command line // to the processing method (normal operation) crunchGroups = new CrunchGroup[] { new CrunchGroup(m_outputFile, m_encodingOutputName, m_resourceFile, m_resourceObjectName, m_inputFiles, m_encodingInputName, m_inputType) }; } if (crunchGroups.Length > 0) { // if any one crunch group is writing to stdout, then we need to make sure // that no progress or informational messages go to stdout or we will output // invalid JavaScript/CSS. Loop through the crunch groups and if any one is // outputting to stdout, set the appropriate flag. for (var ndxGroup = 0; ndxGroup < crunchGroups.Length; ++ndxGroup) { if (string.IsNullOrEmpty(crunchGroups[ndxGroup].Output)) { // set the flag; no need to check any more m_outputToStandardOut = true; break; } } // loop through all the crunch groups for (int ndxGroup = 0; ndxGroup < crunchGroups.Length; ++ndxGroup) { // shortcut CrunchGroup crunchGroup = crunchGroups[ndxGroup]; // process the crunch group int crunchResult = ProcessCrunchGroup(crunchGroup); // if the result contained an error... if (crunchResult != 0) { // if we're processing more than one group, we should output an // error message indicating that this group encountered an error if (crunchGroups.Length > 1) { // non-localized string, so format is not in the resources string errorCode = string.Format(CultureInfo.InvariantCulture, "AM{0:D4}", crunchResult); // if there is an output file name, use it. if (!string.IsNullOrEmpty(crunchGroup.Output)) { WriteError(crunchGroup.Output, StringMgr.GetString("OutputFileErrorSubCat"), errorCode, StringMgr.GetString("OutputFileError", crunchResult) ); } else if (!string.IsNullOrEmpty(m_xmlInputFile)) { // use the XML file as the location, and the index of the group for more info // inside the message WriteError(m_xmlInputFile, StringMgr.GetString("OutputGroupErrorSubCat"), errorCode, StringMgr.GetString("OutputGroupError", ndxGroup, crunchResult) ); } else { // no output file name, and not from an XML file. If it's not from an XML // file, then there really should only be one crunch group. // but just in case, use "stdout" as the output file and the index of the group // in the list (which should probably just be zero) WriteError("stdout", StringMgr.GetString("OutputGroupErrorSubCat"), errorCode, StringMgr.GetString("OutputGroupError", ndxGroup, crunchResult) ); } } // return the error. Only the last one will be used retVal = crunchResult; } } } else { // no crunch groups throw new UsageException(ConsoleOutputMode.Console, "NoInput"); } return retVal; }