//***************************************************************************** //* Object Name: JsMin //***************************************************************************** //* Copyright © GalaSoft Laurent Bugnion 2007 //***************************************************************************** //* Project : GalaSoftLb.Utilities //* Target : .NET Framework 2.0 //* Language/Compiler : C# //* Author : Laurent Bugnion (LBu), GalaSoft //* Web : http://www.galasoft.ch //* Contact info : laurent@galasoft.ch //* Created : 05.02.2007 //***************************************************************************** //* Description: //* See the class definition here under. //* Last base level: BL0002. //* Note: This code is strongly inspired of the C program jsmin, by Douglas Crockford. //* http://www.crockford.com/javascript/jsmin.html //***************************************************************************** //***************************************************************************** //* Imports ******************************************************************* //***************************************************************************** #region Imports using System; using System.IO; using GalaSoftLb.Utilities.Attributes; #endregion namespace GalaSoftLb.Utilities { // Class definition ********************************************************* /// /// Performs a "minimize" operation on JavaScript files. /// It removes unnecessary characters and all comments from the file. /// However, it is not an obfuscator. It doesn't modify the variable names. /// Comments will be removed. Tabs will be /// replaced with a single space. Carriage returns will be replaced with linefeeds. /// Most spaces and linefeeds will be removed. /// This code is strongly inspired of the C program jsmin, by Douglas Crockford. /// JsMin homepage /// [CClassInfo( typeof( JsMin ), strVersion = "V1.1.0", strDate = "20070219213500", strDescription = "Performs a 'minimize' operation on JavaScript files.", strUrlContacts = "http://www.galasoft.ch/contact_en.html", strEmail = "laurent@galasoft.ch" )] public class JsMin { //************************************************************************* //* Enums ***************************************************************** //************************************************************************* #region Enums // ------------------------------------------------------------------------ private enum Action { /// /// Action 1: Output A. Copy A to B. Get the next B. /// OutputA1 = 1, /// /// Action 2: Copy B to A. Get the next B. (Delete A). /// CopyAtoB2 = 2, /// /// Action 3: Get the next B. (Delete B). /// GetNextB3 = 3, } #endregion //************************************************************************* //* Constants ************************************************************* //************************************************************************* #region Constants private const int EOF = -1; private const int TILDE = 126; #endregion //************************************************************************* //* Static attributes ***************************************************** //************************************************************************* #region StaticAttributes #endregion //************************************************************************* //* Attributes ************************************************************ //************************************************************************* #region Attributes private string bakFileExtension = "bak"; private string[] firstLines = null; private int keepFirstLines = 0; // Internal variables private int charA = '\n'; private int charB = '\n'; private int lookAhead = EOF; private bool firstLineOutput = false; #endregion //************************************************************************* //* Properties ************************************************************ //************************************************************************* #region Properties // ------------------------------------------------------------------------ /// /// Extension of the file created for backup. /// This file will be deleted after process, unless the /// parameter keepBakFile is set to true in /// public string BakFileExtension { set { if ( ( value == null ) || ( value.Length == 0 ) ) { value = "bak"; } bakFileExtension = value; } get { return bakFileExtension; } } // ------------------------------------------------------------------------ /// /// Each line in this array will be added to the top output file as comments. /// The lines do not need to have the comment characters. /// public string[] FirstLines { set { firstLines = value; } } // ------------------------------------------------------------------------ /// /// A number specifying how many lines of the original file will be kept /// in the minimized file. This can be used to preserve copyright information, /// the author's name, etc... /// Note: These lines will be added *after* the ones specified /// in . /// public int KeepFirstLines { set { if ( value < 0 ) { value = 0; } keepFirstLines = value; } } #endregion //************************************************************************* //* Static methods ******************************************************** //************************************************************************* #region Static methods #endregion //************************************************************************* //* Constructor & generated code ****************************************** //************************************************************************* #region Constructors // ------------------------------------------------------------------------ /// /// Constructor. /// public JsMin() { } #endregion //************************************************************************* //* Event handlers ******************************************************** //************************************************************************* #region Event handlers #endregion //************************************************************************* //* Methods *************************************************************** //************************************************************************* #region Methods // ------------------------------------------------------------------------ /// /// Converts the input file into a minimized file with the same name. /// /// The absolute path to the file to be minimized. public void Go( string filePath ) { Go( filePath, false ); } // ------------------------------------------------------------------------ /// /// Converts the input file into a minimized file with the same name. /// /// The absolute path to the file to be minimized. /// If true, the input file will be saved in a BAK file. /// The BAK file extension can be set with public void Go( string filePath, bool keepBakFile ) { bool bakFileCreated = false; FileInfo oldFile = null; FileInfo newFile = null; StreamReader input = null; StreamWriter output = null; FileAttributes oldAttributes; try { // Save backup file oldFile = new FileInfo( filePath ); newFile = new FileInfo( filePath ); if ( !oldFile.Exists ) { throw new FileNotFoundException( "File not found", filePath ); } oldAttributes = oldFile.Attributes; oldFile.Attributes = FileAttributes.Normal; string bakFilePath = oldFile.FullName + "." + this.bakFileExtension; if ( File.Exists( bakFilePath ) ) { FileInfo tempFile = new FileInfo( bakFilePath ); tempFile.Attributes = FileAttributes.Normal; tempFile.Delete(); } oldFile.MoveTo( bakFilePath ); bakFileCreated = true; try { input = oldFile.OpenText(); output = newFile.CreateText(); // Process file this.Go( input, output ); } catch { throw; } finally { if ( output != null ) { output.Close(); } if ( input != null ) { input.Close(); } } // Everything went OK! if ( newFile.Exists ) { newFile.Attributes = oldAttributes; } if ( !keepBakFile ) { if ( ( oldFile != null ) && oldFile.Exists ) { oldFile.Delete(); } } } catch ( Exception ex ) { // Restore old file if ( bakFileCreated ) { if ( ( newFile != null ) && newFile.Exists ) { newFile.Attributes = FileAttributes.Normal; newFile.Delete(); } oldFile.MoveTo( filePath ); } throw ex; } } // ------------------------------------------------------------------------ /// /// Minimizes the script code provided by the reader and saves it to the writer. /// /// This method will NOT close the provided reader and writer. /// A reader providing the script code to be minimized. /// The writer to which the minimized code will be saved. public void Go( StreamReader reader, StreamWriter writer ) { // Process file if ( firstLines != null ) { for ( int index = 0; index < firstLines.Length; index++ ) { writer.WriteLine( "// " + firstLines[ index ] ); } } string line = ""; for ( int index = 0; index < keepFirstLines; index++ ) { line = reader.ReadLine(); if ( line != null ) { writer.WriteLine( line ); } } if ( line == null ) { return; } charA = '\n'; PerformAction( Action.GetNextB3, reader, writer ); while ( charA != EOF ) { switch ( charA ) { case ' ': { if ( IsAlphaNumeric( charB ) ) { PerformAction( Action.OutputA1, reader, writer ); } else { PerformAction( Action.CopyAtoB2, reader, writer ); } } break; case '\n': { switch ( charB ) { case '{': case '[': case '(': case '+': case '-': PerformAction( Action.OutputA1, reader, writer ); break; case ' ': PerformAction( Action.GetNextB3, reader, writer ); break; default: if ( IsAlphaNumeric( charB ) ) { PerformAction( Action.OutputA1, reader, writer ); } else { PerformAction( Action.CopyAtoB2, reader, writer ); } break; } } break; default: { switch ( charB ) { case ' ': { if ( IsAlphaNumeric( charA ) ) { PerformAction( Action.OutputA1, reader, writer ); break; } PerformAction( Action.GetNextB3, reader, writer ); } break; case '\n': { switch ( charA ) { case '}': case ']': case ')': case '+': case '-': case '"': case '\'': PerformAction( Action.OutputA1, reader, writer ); break; default: if ( IsAlphaNumeric( charA ) ) { PerformAction( Action.OutputA1, reader, writer ); } else { PerformAction( Action.GetNextB3, reader, writer ); } break; } } break; default: PerformAction( Action.OutputA1, reader, writer ); break; } } break; } } } // ------------------------------------------------------------------------ /// /// Performs different action depending on the parameter. /// Output A. Copy A to B. Get the next B. /// Copy B to A. Get the next B. (Delete A). /// Get the next B. (Delete B). /// /// Specify which action to perform. /// A reader providing the script code to be minimized. /// The writer to which the minimized code will be saved. private void PerformAction( Action whatToDo, StreamReader reader, StreamWriter writer ) { if ( whatToDo == Action.OutputA1 ) { if ( charA != '\n' || firstLineOutput == true ) { writer.Write( (char) charA ); firstLineOutput = true; } } if ( ( whatToDo == Action.OutputA1 ) || ( whatToDo == Action.CopyAtoB2 ) ) { charA = charB; if ( ( charA == '\'' ) || ( charA == '"' ) ) { while ( true ) { writer.Write( (char) charA ); charA = GetChar( reader ); if ( charA == charB ) { break; } if ( charA == '\n' ) { throw new ApplicationException( "Unterminated string literal" ); } if ( charA == '\\' ) { writer.Write( (char) charA ); charA = GetChar( reader ); } } } } if ( ( whatToDo == Action.OutputA1 ) || ( whatToDo == Action.CopyAtoB2 ) || ( whatToDo == Action.GetNextB3 ) ) { charB = NextChar( reader ); if ( ( charB == '/' ) && ( ( charA == '(' ) || ( charA == ',' ) || ( charA == '=' ) || ( charA == '[' ) || ( charA == '!' ) || ( charA == ':' ) || ( charA == '&' ) || ( charA == '|' ) || ( charA == '?' ) ) ) { writer.Write( (char) charA ); writer.Write( (char) charB ); while ( true ) { charA = GetChar( reader ); if ( charA == '/' ) { break; } else { if ( charA == '\\' ) { writer.Write( (char) charA ); charA = GetChar( reader ); } else { if ( charA <= '\n' ) { throw new ApplicationException( "Unterminated Regular Expression literal." ); } } } writer.Write( (char) charA ); } charB = NextChar( reader ); } } } // ------------------------------------------------------------------------ /// /// Watch out for lookahead. If the character is a control character, /// translate it to a space or linefeed. /// /// A reader providing the script code to be minimized. /// Return the next character from the input file. private int GetChar( StreamReader reader ) { int newChar = lookAhead; lookAhead = EOF; if ( newChar == EOF ) { newChar = reader.Read(); } if ( ( newChar >= ' ' ) || ( newChar == '\n' ) || ( newChar == EOF ) ) { return newChar; } if ( newChar == '\r' ) { return '\n'; } return ' '; } // ------------------------------------------------------------------------ /// /// Get the next character without getting it. /// /// A reader providing the script code to be minimized. /// The next character private int PeekChar( StreamReader reader ) { lookAhead = GetChar( reader ); return lookAhead; } // ------------------------------------------------------------------------ /// /// Get the next character, excluding comments. /// is used to see if a '/' is followed by a '/' or '*'. /// /// A reader providing the script code to be minimized. /// The next character. private int NextChar( StreamReader reader ) { int newChar = GetChar( reader ); if ( newChar == '/' ) { int peekChar = PeekChar( reader ); if ( peekChar == '/' ) { while ( true ) { newChar = GetChar( reader ); if ( newChar <= '\n' ) { return newChar; } } } if ( ( peekChar == '/' ) || ( peekChar == '*' ) ) { GetChar( reader ); while ( true ) { switch ( GetChar( reader ) ) { case '*': { if ( PeekChar( reader ) == '/' ) { GetChar( reader ); return ' '; } } break; case EOF: { throw new ApplicationException( "Unterminated comment" ); } } } } return newChar; } return newChar; } // ------------------------------------------------------------------------ /// /// Check if the character is alphanumeric. /// /// The character to be tested. /// True if the character is a letter, digit, underscore, /// dollar sign, or non-ASCII character private bool IsAlphaNumeric( int toTest ) { return ( ( toTest >= 'a' && toTest <= 'z' ) || ( toTest >= '0' && toTest <= '9' ) || ( toTest >= 'A' && toTest <= 'Z' ) || toTest == '_' || toTest == '$' || toTest == '\\' || toTest > TILDE ); } #endregion //************************************************************************* //* Operators ************************************************************* //************************************************************************* #region Operators #endregion } }