Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
234 lines (199 sloc) 5.68 KB
#--
# jsmin.rb - Ruby implementation of Douglas Crockford's JSMin.
#
# This is a port of jsmin.c, and is distributed under the same terms, which are
# as follows:
#
# 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.
#++
require 'strscan'
# = JSMin
#
# Ruby implementation of Douglas Crockford's JavaScript minifier, JSMin.
#
# Author:: Ryan Grove (mailto:ryan@wonko.com)
# Version:: 1.0.0 (2008-03-22)
# Copyright:: Copyright (c) 2008 Ryan Grove. All rights reserved.
# Website:: http://github.com/rgrove/jsmin/
#
# == Example
#
# require 'rubygems'
# require 'jsmin'
#
# File.open('example.js', 'r') {|file| puts JSMin.minify(file) }
#
module JSMin
ORD_LF = "\n"[0].freeze
ORD_SPACE = ' '[0].freeze
class << self
# Reads JavaScript from +input+ (which can be a String or an IO object) and
# returns a String containing minified JS.
def minify(input)
@js = StringScanner.new(input.is_a?(IO) ? input.read : input.to_s)
@a = "\n"
@b = nil
@lookahead = nil
@output = ''
action_get
while !@a.nil? do
case @a
when ' '
if alphanum?(@b)
action_output
else
action_copy
end
when "\n"
if @b == ' '
action_get
elsif @b =~ /[{\[\(+-]/
action_output
else
if alphanum?(@b)
action_output
else
action_copy
end
end
else
if @b == ' '
if alphanum?(@a)
action_output
else
action_get
end
elsif @b == "\n"
if @a =~ /[}\]\)\\"+-]/
action_output
else
if alphanum?(@a)
action_output
else
action_get
end
end
else
action_output
end
end
end
@output
end
private
# Corresponds to action(1) in jsmin.c.
def action_output
@output << @a
action_copy
end
# Corresponds to action(2) in jsmin.c.
def action_copy
@a = @b
if @a == '\'' || @a == '"'
loop do
@output << @a
@a = get
break if @a == @b
if @a[0] <= ORD_LF
raise "JSMin parse error: unterminated string literal: #{@a}"
end
if @a == '\\'
@output << @a
@a = get
if @a[0] <= ORD_LF
raise "JSMin parse error: unterminated string literal: #{@a}"
end
end
end
end
action_get
end
# Corresponds to action(3) in jsmin.c.
def action_get
@b = nextchar
if @b == '/' && (@a == "\n" || @a =~ /[\(,=:\[!&|?{};]/)
@output << @a
@output << @b
loop do
@a = get
if @a == '/'
break
elsif @a == '\\'
@output << @a
@a = get
elsif @a[0] <= ORD_LF
raise "JSMin parse error: unterminated regular expression " +
"literal: #{@a}"
end
@output << @a
end
@b = nextchar
end
end
# Returns true if +c+ is a letter, digit, underscore, dollar sign,
# backslash, or non-ASCII character.
def alphanum?(c)
c.is_a?(String) && !c.empty? && (c[0] > 126 || c =~ /[0-9a-z_$\\]/i)
end
# Returns the next character from the input. If the character is a control
# character, it will be translated to a space or linefeed.
def get
c = @lookahead.nil? ? @js.getch : @lookahead
@lookahead = nil
return c if c.nil? || c == "\n" || c[0] >= ORD_SPACE
return "\n" if c == "\r"
return ' '
end
# Gets the next character, excluding comments.
def nextchar
c = get
return c unless c == '/'
case peek
when '/'
loop do
c = get
return c if c[0] <= ORD_LF
end
when '*'
get
loop do
case get
when '*'
if peek == '/'
get
return ' '
end
when nil
raise 'JSMin parse error: unterminated comment'
end
end
else
return c
end
end
# Gets the next character without getting it.
def peek
@lookahead = get
end
end
end