jsmin.rb を Class 化する

ハイパフォーマンスWebサイト ―高速サイトを実現する14のルール

ハイパフォーマンスWebサイト ―高速サイトを実現する14のルール

これを読みつついろいろいじっていたところ、http://www.crockford.com/javascript/jsmin.rb があったので使ってみました。確かに便利なのですが、折角なので rake で deploy する際に使用できるようにクラス化してました。そうすれば rake ファイル中でこんな事ができるようになります。

# -*- ruby -*-

require 'fileutils'
require 'jsmin'

def minify
  puts "Minify JavaScript files by jsmin"

  Dir.glob('*.js').each do |js|
    JsMin.new(js, File.join(dirs[:minified], js)).minify
  end
end
...

で、以下がそのコードです。

#
# jsmin.rb 2008-06-08
# Author: UEDA Hiroyuki <BSDmad@gmail.com>
# 
# This work is originated from jsmin.rb by Uladzislau Latynski.
# The purpose of translation from jsmin.rb to jsmin.rb ^^;) is
# for using the feature as a class. As a result, you can deploy
# minified JavaScript files with this class as follows:
#
#     (snip...) 
#     File.glob('*.js').each do |file|
#       JsMin.new(file, 'output/' + file).minify
#     end
#     (snip...) 
#
#
# jsmin.rb 2007-07-20
# Author: Uladzislau Latynski
# This work is a translation from C to Ruby of jsmin.c published by
# Douglas Crockford.  Permission is hereby granted to use the Ruby
# version under the same conditions as the jsmin.c on which it is
# based.
#
# /* jsmin.c
#    2003-04-21
#
# Copyright (c) 2002 Douglas Crockford  (www.crockford.com)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# The Software shall be used for Good, not Evil.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

class JsMin
  
  EOF = -1

  def initialize(src, dst = $stdout)
    @src = src
    @dst = dst
    @input = nil
    @output = nil
    @the_a = ""
    @the_b = ""
  end

  def minify
    begin
      @input  = File.open(@src, 'r')
      @output = File.open(@dst, 'w')
      jsmin
    rescue
      p $!
      p $@
    ensure
      @input.close if @input
      @output.close if @output
    end
  end

  private
  
  def isAlphanum(c)
    return false if !c || c == EOF
    return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
      (c >= 'A' && c <= 'Z') || c == '_' || c == '$' ||
      c == '\\' || c[0] > 126)
  end
  
  def get
    c = @input.getc
    return EOF if(!c)
    c = c.chr
    return c if (c >= " " || c == "\n" || c.unpack("c") == EOF)
    return "\n" if (c == "\r")
    return " "
  end
  
  # Get the next character without getting it.
  def peek
    lookaheadChar = @input.getc
    @input.ungetc(lookaheadChar)
    return lookaheadChar.chr
  end
  
  # mynext -- get the next character, excluding comments.
  # peek() is used to see if a '/' is followed by a '/' or '*'.
  def mynext
    c = get
    if (c == "/")
      if (peek == "/")
        while(true)
          c = get
          if (c <= "\n")
            return c
          end
          end
          end
          if(peek == "*")
              get
              while(true)
                  case get
            when "*"
            if (peek == "/")
              get
              return " "
            end
          when EOF
            raise "Unterminated comment"
          end
        end
      end
    end
    return c
  end
  
  
  # action -- do something! What you do is determined by the argument: 1
  # Output A. Copy B to A. Get the next B. 2 Copy B to A. Get the next B.
  # (Delete A). 3 Get the next B. (Delete B). action treats a string as a
  # single character. Wow! action recognizes a regular expression if it is
  # preceded by ( or , or =.
  def action(a)
    result = ""
    if (a==1)
      result += @the_a
    end
    if (a==1 || a==2)
      @the_a = @the_b
      if (@the_a == "\'" || @the_a == "\"")
        while (true)
          result += @the_a
          @the_a = get
          break if (@the_a == @the_b)
          raise "Unterminated string literal" if (@the_a <= "\n")
          if (@the_a == "\\")
            result += @the_a
            @the_a = get
          end
        end
      end
    end
    if(a==1 || a==2 || a==3)
        @the_b = mynext
        if (@the_b == "/" && (@the_a == "(" || @the_a == "," || @the_a == "=" ||
           @the_a == ":" || @the_a == "[" || @the_a == "!" ||
           @the_a == "&" || @the_a == "|" || @the_a == "?" ||
           @the_a == "{" || @the_a == "}" || @the_a == ";" ||
           @the_a == "\n"))
          result = result + @the_a + @the_b
        while (true)
          @the_a = get
          if (@the_a == "/")
            break
          elsif (@the_a == "\\")
            result += @the_a
            @the_a = get
          elsif (@the_a <= "\n")
            raise "Unterminated RegExp Literal"
          end
          result += @the_a
        end
        @the_b = mynext
      end
    end
    @output.print result
  end
  
  # jsmin -- Copy the input to the output, deleting the characters which are
  # insignificant to JavaScript. Comments will be removed. Tabs will be
  # replaced with spaces. Carriage returns will be replaced with linefeeds.
  # Most spaces and linefeeds will be removed.
  def jsmin
    @the_a = "\n"
    action(3)
    while (@the_a != EOF)
      case @the_a
      when " "
        if (isAlphanum(@the_b))
          action(1)
        else
          action(2)
        end
      when "\n"
        case (@the_b)
        when "{","[","(","+","-"
          action(1)
        when " "
          action(3)
        else
          if (isAlphanum(@the_b))
            action(1)
          else
            action(2)
          end
        end
      else
        case (@the_b)
        when " "
          if (isAlphanum(@the_a))
            action(1)
          else
            action(3)
          end
        when "\n"
          case (@the_a)
          when "}","]",")","+","-","\"","\\", "'", '"'
            action(1)
          else
            if (isAlphanum(@the_a))
              action(1)
            else
              action(3)
            end
          end
        else
          action(1)
        end
      end
    end
  end
end