I needed a thread-safe JSMin library for compressing javascripts on the fly on UploadBooth, so I took an existing ruby implementation and made it thread safe. I don’t think there was license defined when I got it, so I am re-releasing it as-is.

[code lang=”ruby”]
require ‘monitor’

class JSMin
EOF = -1
include MonitorMixin

# 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.
# thread safe
def minimize(jstext)
synchronize do
@theA = “”
@theB = “”
@current = 0
@output = “”

@text = jstext
@theA = “\n”
action(3)
while (@theA != JSMin::EOF)
case @theA
when ” ”
if (isAlphanum(@theB))
action(1)
else
action(2)
end
when “\n”
case (@theB)
when “{“,”[“,”(“,”+”,”-”
action(1)
when ” ”
action(3)
else
if (isAlphanum(@theB))
action(1)
else
action(2)
end
end
else
case (@theB)
when ” ”
if (isAlphanum(@theA))
action(1)
else
action(3)
end
when “\n”
case (@theA)
when “}”,”]”,”)”,”+”,”-“,”\””,”\\”, “‘”, ‘”‘
action(1)
else
if (isAlphanum(@theA))
action(1)
else
action(3)
end
end
else
action(1)
end
end
end
@output
end
end

private
# isAlphanum — return true if the character is a letter, digit, underscore,
# dollar sign, or non-ASCII character
def isAlphanum(c)
return false if !c || c == JSMin::EOF
return ((c >= ‘a’ && c <= 'z') || (c >= ‘0’ && c <= '9') || (c >= ‘A’ && c <= 'Z') || c == '_' || c == '$' || c == '\\' || c[0] > 126)
end

# get — return the next character from stdin. Watch out for lookahead. If
# the character is a control character, translate it to a space or linefeed.
# thread safe
def get
return JSMin::EOF if @current>(@text.length-1)
c = @text[@current]
@current += 1
c = c.chr
return c if (c >= ” ” || c == “\n” || c.unpack(“c”) == JSMin::EOF)
return “\n” if (c == “\r”)
return ” ”
end

# Get the next character without getting it.
def peek
lookaheadChar = @text[@current]
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 JSMin::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) if(a==1) @output << @theA end if(a==1 || a==2) @theA = @theB if (@theA == “\'” || @theA == “\””) while (true) @output << @theA @theA = get break if (@theA == @theB) raise “Unterminated string literal” if (@theA <= “\n”) if (@theA == “\\”) @output << @theA @theA = get end end end end if(a==1 || a==2 || a==3) @theB = mynext if (@theB == “/” && (@theA == “(” || @theA == “,” || @theA == “=” || @theA == “:” || @theA == “[” || @theA == “!” || @theA == “&” || @theA == “|” || @theA == “?” || @theA == “{” || @theA == “}” || @theA == “;” || @theA == “\n”)) @output << @theA @output << @theB while (true) @theA = get if (@theA == “/”) break elsif (@theA == “\\”) @output << @theA @theA = get elsif (@theA <= “\n”) raise “Unterminated RegExp Literal” + @output end @output << @theA end @theB = mynext end end end end [/code]