233 lines
5.7 KiB
Ruby
233 lines
5.7 KiB
Ruby
#--
|
|
# 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
|